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
claudeEverything 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:
PluginHeartbeatis created at run time byActivator.CreateInstance<T>()rather than withnew(F001).
That by itself is rename-safe — nothing reads the name. It only breaks if the new instance is then serialised, mapped by an ORM, or populated by a container, since those bind by property name. Demeanor can see the construction; it cannot see far enough to know which of those happens next. That is why this is a decision and not an automatic protection.
The remediation says what to look for:
Trace what happens to the instance after it is created. If it is serialised, mapped by an ORM, or resolved by name anywhere downstream, mark the type[Obfuscation(Exclude=true, ApplyToMembers=true)]. If it is only used through its compiled type, resolve the finding as safe-to-rename.So the assistant reads the call site rather than guessing:
var beat = Activator.CreateInstance<PluginHeartbeat>(); beat.PluginName = "legacy-audit"; beat.Cycle = cycle; beat.ReportedAt = DateTime.UtcNow; return JsonSerializer.Serialize(beat);
That settles it. The instance is serialised, and the collector on the other end binds by property name — so the answer is protect, not safe-to-rename. The assistant recommends the attribute the remediation names.
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.
Protect PluginHeartbeat, or resolve it as safe-to-rename?
Protect it.
Fix applied
The assistant edits Legacy/LegacyPlugin.cs — one attribute, and theusing it needs:
+using System.Reflection; using System.Text.Json; /// <summary> /// Heartbeat payload posted to the plugin collector, which binds by property name. /// </summary> +[Obfuscation(Exclude = true, ApplyToMembers = true)] public sealed class PluginHeartbeat
The assistant rebuilds and re-audits to confirm the fix. The rebuild is not a formality — demeanor audit reads compiled metadata, so an edit is invisible to it until it is in the assembly:
Build succeeded.
0 Warning(s)
0 Error(s)
16 findings -> 15. Needs your decision: 1 -> 0.
PluginHeartbeat now carries its own instruction, so the rule has
nothing left to ask. Nothing else moved.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 PluginHeartbeat finding earlier in the session was a needs-decision one — a reflectively-constructed type whose safety depends on what happens to the instance, which 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 — “payloads our plugin layer builds reflectively get protected” — could fire again on the next payload type 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 PluginHeartbeat, but the same pattern will come up the next time someone adds a payload type the plugin host builds reflectively. 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": "myteam-reflective-payloads",
"version": 1,
"predicate-vocab": 1,
"kind": "type-protection",
"summary": "Types the plugin layer builds through Activator.CreateInstance(), so each one carries an explicit decision rather than inheriting a default.",
"reasoning": "Reflective construction is rename-safe on its own; what breaks is what happens to the instance afterwards. Our plugin payloads are serialised to the collector, which binds by property name, so the team's answer for this shape is always protect. Recorded so the next payload type does not have to re-argue it.",
"remediation": "Apply [Obfuscation(Exclude=true, ApplyToMembers=true)] to the payload type. ObfuscationAttribute is System.Reflection.ObfuscationAttribute.",
"severity": "informational",
"provenance": "project",
"license": "any",
"when": {
"predicate": "il-calls",
"args": { "methods": ["CreateInstance"], "declaring-type": "Activator", "capture": "generic-arg-0" }
},
"then": { "freeze": "report", "report-as": "myteam-reflective-payloads" }
} Yes, save it.
The assistant writes .demeanor/patterns/myteam-reflective-payloads.json, re-runs the audit to confirm the rule fires on PluginHeartbeat as informational 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
PluginHeartbeatcase states what the finding cannot settle on its own, reads the call site to settle it, and recommends the answer that follows — 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
PluginHeartbeatfinding went from “needs your decision” to gone — 16 findings and one decision became 15 and none; 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