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

You

/obfuscate

The assistant confirms Demeanor is installed, confirms the license is set, and builds Release. No output shown — none of it needed your attention.

PHASE 1

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:

PatternTypes
System.Text.Json (reflection-mode)AuditRecord
[Serializable] typeLegacyReportDto
Property names referenced as strings in codeCatalogJsonContext, Product, Order, OrderItem, LegacyWebhookPayload
MVC controllerHealthController
EF Core entity / DbContextCatalogDbContext, Product, Order, OrderItem
Configuration binding (IOptions)CatalogOptions
Newtonsoft.Json typeLegacyWebhookPayload

Needs your decision — one thing to decide:

PluginHeartbeat is created at run time byActivator.CreateInstance<T>() rather than with new (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:

Legacy/LegacyPlugin.cs
 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.SerializeAuditRecord uses a reflection-path JsonSerializer.Serialize call (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?

You

Protect it.

PHASE 2

Fix applied

The assistant edits Legacy/LegacyPlugin.cs — one attribute, and theusing it needs:

Legacy/LegacyPlugin.cs
+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.
PHASE 3

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
Types11 / 28 (39%)
Methods79 / 251 (31%)
Fields57 / 155 (36%)
Properties5 / 53 (9%)
Parameters86 / 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 encrypted84
Constants encrypted43

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?

You

Yes

PHASE 4

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.

PHASE 5

Verification

CheckResult
WatermarkEnterprise
String encryption
Anti-tamper
Anti-debug
SuppressIldasm
Exclusions matchedEF, IOptions, [Serializable], Newtonsoft, MVC
OrderSummary JSON getters preservedget_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.

PHASE 6

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_LICENSE must be a build-system secret (GitHub Actions secret, Azure DevOps variable, etc.) — never commit the key to the repo.
  • No extra install step — dotnet restore pulls WiseOwl.Demeanor.MSBuild via the PackageReference.
  • Keep *.report.json as a build artifact so future crash reports can be decoded with demeanor 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 }}
PHASE 7

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:

Assistant

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" }
}
You

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 PluginHeartbeat case 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 PluginHeartbeat finding 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 →

Buy EnterpriseStart with the CLI

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