Conversational Audit Walkthrough
A real /obfuscate session on a small ASP.NET Core sample, exactly as it happened.
Optional — this page shows the conversational workflow. The same audit runs as a plain CLI command: demeanor audit MyApp.dll prints the same findings you see below, and every fix shown here can be applied by hand. Demeanor’s audit is the intelligence — the assistant is a conversational relay. See Getting Started for the standalone path.
Requires your own Claude subscription — not included with Demeanor. The session was recorded with Claude, but Demeanor’s MCP server is spec-compliant — any MCP-capable assistant (Claude Desktop, Cursor, Windsurf, Continue.dev, VS Code MCP extensions, and others) drives the same loop.
The sample
CatalogService is a small ASP.NET Core 10 web service — about 250 lines of C#. It uses the most common .NET serialization and data-access patterns (EF Core, System.Text.Json, Newtonsoft, MVC, an [Serializable] legacy DTO) and deliberately contains one thing the assistant will ask you about, plus two it flags for your information. Running /obfuscate against this sample is what produced every block below.
From the project directory, start Claude Code:
cd samples/CatalogService
claude Everything below is what the assistant does between your first slash command and your next decision point.
The session
/obfuscate
The assistant confirms Demeanor is installed, confirms the license is set, and builds Release. No output shown — none of it needed your attention.
Audit results
The assistant calls into Demeanor’s MCP server and relays the audit.
Demeanor found 7 such patterns in CatalogService and will keep the original names — no action needed from you:
| Pattern | Types |
|---|---|
| System.Text.Json (reflection-mode) | AuditRecord |
| [Serializable] type | LegacyReportDto |
| Property names referenced as strings in code | CatalogJsonContext, Product, Order, OrderItem, LegacyWebhookPayload |
| MVC controller | HealthController |
| EF Core entity / DbContext | CatalogDbContext, Product, Order, OrderItem |
| Configuration binding (IOptions) | CatalogOptions |
| Newtonsoft.Json type | LegacyWebhookPayload |
Needs your decision — one thing to decide:
MapGet("/orders/{id:int}/summary")returnsOrderSummary, whose properties will be renamed (F005).
OrderSummaryisn’t registered inCatalogJsonContext(the source-generated JSON context) and no code references its properties by name, so Demeanor has no signal that its property names are part of a public contract. After obfuscation, the endpoint would emit{"a": ..., "b": ... }instead of the readable JSON clients expect.
Two ways to fix:
The assistant recommends #1 — keeps one consistent mechanism across the project.
Informational
LegacyPlugin.SerializeAuditRecorduses a reflection-pathJsonSerializer.Serializecall (F009). Not a renaming risk. It only matters if you publish Native AOT — the IL2026/IL3050 analyzer warnings (trimmer/AOT compatibility diagnostics) will surface because reflection-based serialization can’t be statically analyzed.
Which fix for OrderSummary — #1 (source-gen context) or #2 (attribute on the DTO)?
#1
Fix applied
The assistant edits Serialization/CatalogJsonContext.cs — one attribute added:
[JsonSerializable(typeof(Product))] [JsonSerializable(typeof(Order))] [JsonSerializable(typeof(OrderItem))] +[JsonSerializable(typeof(OrderSummary))] [JsonSerializable(typeof(List<Product>))] [JsonSerializable(typeof(List<Order>))] public partial class CatalogJsonContext : JsonSerializerContext { }
The assistant rebuilds and re-audits to confirm the fix:
Build succeeded.
0 Warning(s)
0 Error(s)
OrderSummary is now auto-protected — the source-gen context
registration made it visible to PropertyUsageAnalyzer. All 24 types
safe to obfuscate. Dry-run results
CatalogService is an executable, so the assistant passes include_publics=true to also rename public types — nothing external references this assembly.
| Renamed / Total | |
|---|---|
| Types | 11 / 28 (39%) |
| Methods | 79 / 251 (31%) |
| Fields | 57 / 155 (36%) |
| Properties | 5 / 53 (9%) |
| Parameters | 86 / 193 (44%) |
| Methods with control-flow obfuscation (the order of operations inside each method body becomes unreadable in a decompiler) | 90 / 242 (37%) |
| Calls hidden from static analysis (decompilers can’t resolve which methods call which) | 9 / 9 |
| Strings encrypted | 84 |
| Constants encrypted | 43 |
The 9% property-rename rate is the signal auto-protection worked as intended — nearly every property belongs to an EF entity, a source-gen JSON DTO, a [Serializable] or Newtonsoft type, or the IOptions POCO. The 31% method-rename rate similarly reflects the MVC and Minimal API surface being preserved.
Ready to run the real obfuscation?
Yes
Obfuscation complete
Obfuscated DLL: bin/Release/net10.0/Demeanor/CatalogService.dll (49.6 KB) A quick smoke test before shipping: launch the obfuscated DLL and curl /orders/1/summary to confirm the JSON shape, then /health to confirm the controller still routes.
Verification
| Check | Result |
|---|---|
| Watermark | Enterprise |
| String encryption | ✓ |
| Anti-tamper | ✓ |
| Anti-debug | ✓ |
| SuppressIldasm | ✓ |
| Exclusions matched | EF, IOptions, [Serializable], Newtonsoft, MVC |
OrderSummary JSON getters preserved | get_Id, get_CustomerName, get_LineCount, get_Total |
Exclusions hit the right types: EF entities (Product, Order, OrderItem, CatalogDbContext), CatalogOptions (IOptions), LegacyReportDto ([Serializable]), LegacyWebhookPayload (Newtonsoft), HealthController (MVC), plus the compiler-generated anonymous type.
OrderSummary’s property getters are absent from the renamed list, so the JSON output still emits {"id","customerName","lineCount","total"} at runtime. The record’s Equals / GetHashCode / ToString / Deconstruct methods are renamed since they’re not part of the JSON contract.
CI/CD integration
Wiring build-time obfuscation into the .csproj means every Release build produces an obfuscated DLL automatically — no manual demeanor step in CI, no drift between dev builds and shipping builds. The assistant edits CatalogService.csproj:
<ItemGroup>
<PackageReference Include="WiseOwl.Demeanor.MSBuild" Version="6.*" />
</ItemGroup> Just the <PackageReference> — the package auto-enables obfuscation for Release builds and auto-skips test projects via IsTestProject.
CI requirements:
DEMEANOR_LICENSEmust be a build-system secret (GitHub Actions secret, Azure DevOps variable, etc.) — never commit the key to the repo.- No extra install step —
dotnet restorepullsWiseOwl.Demeanor.MSBuildvia thePackageReference. - Keep
*.report.jsonas a build artifact so future crash reports can be decoded withdemeanor deobfuscate. - For release-to-release JSON compatibility, pass the prior release’s report to the next build via
<DemeanorPriorReport>to preserve the name mapping for any obfuscated fields that ship across versions.
GitHub Actions snippet:
- run: dotnet build -c Release
env:
DEMEANOR_LICENSE: ${{ secrets.DEMEANOR_LICENSE }} Promoting the decision to a project rule
The OrderSummary finding earlier in the session was a needs-decision one — an unregistered handler-return type that the audit can’t resolve on its own. The team decided to register it in the source-generated context. That fix was made in source, so it sticks.
But the pattern — “handler return types should be registered in the source-gen context” — could fire again on the next handler added to this service, and on every other handler in every other service this team owns. Re-running the conversation each time is waste. The assistant offers to capture the decision as a project rule:
That fix resolves OrderSummary, but the same pattern will come up the next time someone adds a handler that returns a new DTO. Want me to save your decision as a project rule under .demeanor/patterns/? The audit will then advise on it automatically — here and in CI — without re-running this conversation.
Proposed rule:
{
"id": "catalog.handler-return-types-must-register",
"title": "Handler return types should be registered in the source-gen JSON context",
"severity": "advisory",
"match": {
"handlerReturnType": true,
"registeredInJsonSerializerContext": false
},
"rationale": "Unregistered handler return types fall back to the reflection-path serializer, which both breaks Native AOT and prevents the audit from proving property protection. Decided 2026-05-11 in #1287."
} Yes, save it.
The assistant writes .demeanor/patterns/handler-return-types-must-register.json, re-runs the audit to confirm the rule fires on the newly-added OrderSummary registration as advisory rather than needs-decision, and the file shows up in the PR diff alongside the source change. The next audit run — on any teammate’s machine, in CI, six months from now — applies this rule without needing the assistant.
What this demonstrates
- Seven framework patterns fire automatically. Demeanor does the hard work before the assistant says a word — Phase 1’s seven-row table is produced by the audit, not by the assistant.
- The one finding that needed a human decision picks up with a recommendation and rationale. The
OrderSummarycase presents three valid options and flags the cleanest one (register in the source-gen context) — not “just trust me.” - The assistant waits for approval before editing, before dry-running, and before writing files. Every phase boundary is a yes/no checkpoint.
- Re-running the audit after the edit is what proves the fix, not the assistant’s say-so. The
OrderSummaryfinding flipped from “needs your decision” to “handled automatically”; the dry-run confirms zero properties renamed on the DTO shape. - The decision left an artifact. The pattern that came up in this conversation now lives in
.demeanor/patterns/as a project rule. Future audits — including the unattended CI build — apply it automatically. The conversation produced something durable, then stepped out of the way.
Like what you just read?
Enterprise is a per-company subscription. Unlimited developers, unlimited build machines. The audit you just watched is the same one that runs against your code the moment you install — whether you drive it from the CLI or from your AI assistant. See pricing →
Running it on your own code
You don’t need this particular sample. If you don’t use an AI assistant, install Demeanor per Getting Started and run demeanor audit MyApp.dll — you will see the same categorized list you saw in Phase 1.
If you do use an MCP-capable assistant, install Demeanor, open your project in the assistant, and invoke /obfuscate (Claude Code) or ask the assistant to audit the assembly. The assistant will call into Demeanor’s opt-in MCP server, relay the findings, propose fixes, and make the edits with your approval — the same flow you just read.
Next steps
- Getting Started — the canonical five-step CLI guide
- Conversational Workflow — setup for customers who already use an MCP-capable assistant
- Rules — how the layered rule store works and what a project rule looks like
- Decisions & CI — how the conversation produces durable artifacts and how CI consumes them
- Exclusions Guide — framework-by-framework exclusion patterns and the
[JsonSerializable]recipe [ObfuscationAttribute]Features — per-feature control inside source