Getting Started
Five steps from first install to fully integrated CI builds. Install the NuGet package, run the audit, apply any fixes it recommends, obfuscate, wire it into MSBuild.
Step 1 — Install
1a. License (Enterprise)
Set your license key as the DEMEANOR_LICENSE environment variable on whichever machine(s) you build on — CI, dev box, or both.
Windows (persistent):
setx DEMEANOR_LICENSE "your-enterprise-key" Linux / macOS:
export DEMEANOR_LICENSE="your-enterprise-key" Close and reopen any terminal afterward so the new variable is picked up. Get an Enterprise license here. Community tier (free) requires no license key — skip to 1b.
1b. Install the obfuscator
dotnet tool install -g WiseOwl.Demeanor One command. Cross-platform. Puts demeanor on your PATH.
1c. Validate the install
demeanor license This prints the licensee name, tier, and expiration date. The first invocation also triggers a one-time bootstrap that installs the companion inspect tool.
Confirm both tools are installed:
dotnet tool list -g You should see two entries:
Package Id Version Commands
-------------------------------------------
wiseowl.demeanor 6.0.4 demeanor
wiseowl.inspector 6.0.4 inspect Step 2 — Audit before you obfuscate
Always start here. The audit reads your assemblies without modifying anything and reports what Demeanor will auto-protect and what it has questions about. It catches most patterns that would cause a post-obfuscation runtime surprise — reflection, serialization, data binding, DI conventions, etc.
Publish your app normally (dotnet publish -c Release), open a terminal in the publish folder, and run:
demeanor audit MyApp.dll --include-deps Replace MyApp.dll with your entry assembly. For a .NET 8+ app, the .dll next to MyApp.exe is the actual managed assembly. The --include-deps flag tells audit to also analyze co-located private dependencies, so the set it reports on matches what Step 3 will obfuscate.
Example audit output
Here’s what the audit looks like on a sample ASP.NET Core app:
Audit: CatalogService (22 types)
WHAT DEMEANOR WILL DO FOR YOU
These patterns are handled automatically — no action needed.
json-serialization 1 type(s)
Serializable 1 type(s)
property-usage 5 type(s)
mvc-controller 1 type(s)
efcore-dbcontext 4 type(s)
ioptions-binding 1 type(s)
newtonsoft-json 1 type(s)
CODE QUALITY ADVISORIES
minimal-api-handler — MapGet "/products/{id:int}" returns Product;
properties already protected. No action needed.
minimal-api-handler — MapGet "/orders/{id:int}/summary" returns
OrderSummary; properties will be renamed.
reflection-json-call — LegacyPlugin.SerializeAuditRecord uses the
reflection-path JsonSerializer.Serialize overload;
breaks Native AOT (IL2026/IL3050).
SUMMARY
Total types: 22
Auto-protected: 7
Required changes: 0
Safe to obfuscate: 22 How to read it
- WHAT DEMEANOR WILL DO FOR YOU — Framework patterns Demeanor recognized and will protect for you automatically. Nothing for you to do.
- CODE QUALITY ADVISORIES — Informational. The first here is already covered (no action). The other two are things the obfuscation itself would tolerate but are worth fixing before you ship — a public DTO not registered with the source-gen JSON context, and a reflection-path serialization call that won’t survive Native AOT.
- SUMMARY — The number that matters is Required changes. If it’s zero, go straight to Step 3.
If you want the audit as JSON (for archiving or sending to support):
demeanor audit MyApp.dll --include-deps --json > audit.json For the full conversation view of this same run, see the CatalogService walkthrough.
Step 3 — Obfuscate interactively
From the same publish folder:
demeanor MyApp.dll --include-deps --report --verbose What the flags do:
--include-deps— Also obfuscates co-located private dependencies, so cross-assembly name references stay consistent.--report— Writes a JSON name-mapping file. Keep this; you’ll need it to deobfuscate stack traces from production.--verbose— Per-phase stats, plus full stack trace on any error.
Third-party NuGet packages (strong-named assemblies) are skipped automatically.
Example obfuscation output
Obfuscation complete (dry run — no files written).
Assemblies processed: 1
Types renamed: 8 of 22 (36%)
Methods renamed: 31 of 74 (41%)
Fields renamed: 2 of 4 (50%)
Properties renamed: 0 of 24 (0%)
Parameters renamed: 18 of 26 (69%)
Strings encrypted: 14
Methods CFG-obfuscated: 12 of 48 (25%)
Automatic decisions:
[FrameworkPatternAutoProtected] Preserved 12 framework-pattern types
[CustomAttributeBlobRewritten] Rewrote 5 custom-attribute blobs How to read it
- Per-phase counters (Proxied, Encrypted, Injected) report what was applied. These run unconditionally on every eligible site — full Enterprise hardening regardless of rename percentages.
- Rename percentages will look modest on a web app, and that’s correct. Properties: 0 of 43 (0%) looks alarming until you realize every property in a web app is on a DTO, an EF entity, an MVC action input, or a SignalR hub argument — all framework-bound by name at runtime. Renaming any would break the app.
- Where the real protection comes from on a web app: string encryption, integer-constant encryption, control-flow flattening, anti-tamper, anti-debug, and call-hiding proxies. None of these depend on renaming, and all of them ran on every eligible site.
- Automatic decisions at the bottom explains why the rename percentages are what they are — one line per framework pattern that opted certain types out of renaming.
Output goes to a subfolder named Demeanor by default (use --out <dir> to change). Copy the obfuscated DLLs into your deployment layout and run the app. If it behaves identically, move to Step 5. If anything is off, see Step 4.
Step 4 — If your obfuscated app misbehaves
Almost always a reflection or serialization pattern the audit didn’t flag. The fix is to tell Demeanor to leave the affected type or member alone. Two approaches:
Preferred: [Obfuscation] attribute in source
Add the standard System.Reflection.ObfuscationAttribute to the type or member:
using System.Reflection;
[Obfuscation(Exclude = true, ApplyToMembers = true)]
public class MyDtoThatGetsSerialized
{
public string Name { get; set; }
public int Age { get; set; }
} Or on a single member:
[Obfuscation(Exclude = true)]
public string SomeNameLookedUpByReflection { get; set; } This is the right long-term fix because:
- The exclusion lives next to the code that needs it — future edits can’t accidentally remove it.
- It survives into CI/MSBuild integration with zero extra config.
- The reason is self-documenting — add a comment explaining what depends on the name.
Quick diagnostic: CLI --exclude
Useful for iterating without rebuilding from source:
demeanor MyApp.dll --include-deps --report --verbose --exclude "MyNamespace.MyType" Or a regex match for a group (repeatable):
demeanor MyApp.dll --include-deps --report --verbose --xr "MyNamespace\.Dtos\..*" Once an exclusion fixes the behavior, move it into source as an [Obfuscation] attribute so it doesn’t have to be re-specified on every build. See the Exclusions Guide for full details.
Step 5 — Integrate into your build
Only do this once Step 3 produces a working obfuscated app. The MSBuild path uses the same engine, but diagnosing issues is much easier interactively than from a CI log.
From your project directory:
demeanor init demeanor init adds a PackageReference to WiseOwl.Demeanor.MSBuild and an <Obfuscate>true</Obfuscate> property guarded on Release builds. The package bundles its own obfuscator binary, so build machines need zero global-tool installs — dotnet restore plus dotnet build -c Release is enough.
Then build
dotnet build -c Release Obfuscation runs automatically on Release builds; Debug builds stay unmodified.
Manual alternative
If you prefer to see the changes explicitly, demeanor init writes this snippet into your .csproj:
<ItemGroup>
<PackageReference Include="WiseOwl.Demeanor.MSBuild" Version="6.0.4" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Obfuscate>true</Obfuscate>
</PropertyGroup> You can add this by hand if you prefer. The WiseOwl.Demeanor global tool and the WiseOwl.Demeanor.MSBuild package ship as a matched pair from one build pipeline, so any current version works — pick whichever version you want the build to run against.
Useful MSBuild properties (all optional)
| Property | Default | Description |
|---|---|---|
<Obfuscate> | false | Enables obfuscation for this build. |
<DemeanorIncludeDeps> | false | Also obfuscate co-located private dependencies. |
<DemeanorReport> | true | Emit .report.json for stack-trace decoding. |
<DemeanorQuiet> | true | Suppress non-error output. Set to false for CI logs with per-phase stats. |
MSBuild excludes
If you need CLI-style excludes from MSBuild (instead of [Obfuscation] attributes):
<ItemGroup>
<DemeanorExclude Include="MyNamespace.MyType" />
<DemeanorExcludeRegex Include="MyNamespace\.Dtos\..*" />
<DemeanorExcludeAssembly Include="ThirdParty.Library" />
</ItemGroup> Any [Obfuscation] attributes you added during Step 4 carry over with zero extra config — the engine reads them from the assembly itself. The MSBuild package respects DEMEANOR_LICENSE the same way the CLI does.
Wire Demeanor into your real build.
Ready to wire Demeanor into your real build? Enterprise is $2,999/year, per-company, unlimited developers and CI machines. No seat counting, no activation server, no phone-home.
Using this with an AI assistant (optional)
If you already use an MCP-capable assistant (Claude Code, Claude Desktop, Cursor, Windsurf, Continue.dev, or a VS Code MCP extension), Demeanor’s opt-in MCP server lets you drive the same audit as a chat. Requires your own Claude subscription — not included with Demeanor.