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
JSON reflection-path (properties referenced by JsonSerializer<T>)AuditRecord
[Serializable] (BinaryFormatter / XML formatters resolve property names at runtime)LegacyReportDto
Property-usage (property names appear as string literals in code)CatalogJsonContext, Product, Order, OrderItem, LegacyWebhookPayload
MVC controller routingHealthController
EF Core entities / DbContextCatalogDbContext, Product, Order, OrderItem
IOptions<T> configuration bindingCatalogOptions
Newtonsoft.Json public-property contractLegacyWebhookPayload

Needs your decision — one thing to decide:

MapGet("/orders/{id:int}/summary") returns OrderSummary, whose properties will be renamed (F005).

OrderSummary isn’t registered in CatalogJsonContext (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:

  1. Add [JsonSerializable(typeof(OrderSummary))] to CatalogJsonContext — consistent with how Product, Order, and OrderItem are already registered. Also makes this endpoint ready for Native AOT (ahead-of-time compilation that strips reflection-only code paths).
  2. [Obfuscation(Exclude = true, ApplyToMembers = true)] on the OrderSummary type — works, but introduces a second mechanism for preserving JSON contracts in a codebase that already uses source-gen for this purpose.

The assistant recommends #1 — keeps one consistent mechanism across the project.

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.

Which fix for OrderSummary — #1 (source-gen context) or #2 (attribute on the DTO)?

You

#1

PHASE 2

Fix applied

The assistant edits Serialization/CatalogJsonContext.cs — one attribute added:

Serialization/CatalogJsonContext.cs
 [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.
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%)
CFG-obfuscated methods (control-flow flattening, which scrambles the order and shape of basic blocks inside a method body so decompilers can’t reconstruct the original logic)90 / 242 (37%)
Call sites proxied (intra-assembly method calls routed through relay methods so decompilers can’t see the real target)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>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <Obfuscate>true</Obfuscate>
</PropertyGroup>

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

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 OrderSummary case 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 OrderSummary finding flipped from “needs your decision” to “handled automatically”; the dry-run confirms zero properties renamed on the DTO shape.

Like what you just read? Enterprise is $2,999/year.

Per-company licensing. 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.

Buy Enterprise — $2,999/yr Start 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