How Demeanor decides what to protect

Every decision the audit makes — auto-protect this type, ask about that method, flag a code-quality concern — comes from a rule. Rules ship with Demeanor, your project adds its own, and those live in your repo as plain JSON.

The short version. The audit runs on its own using Demeanor’s built-in rules. Your project can add its own rules in a .demeanor/ folder that you commit to git. Each rule has a severity that tells Demeanor whether to act silently, mention it for review, or protect the match and ask you about it. Rules are how the audit’s judgements stay consistent across runs, across developers, and across CI.

Why the rule store exists

The audit’s job is to recognise patterns that interact badly with obfuscation — a type whose name is looked up by a serializer, a method invoked by name from a framework, a property bound to a UI element. The audit recognises those patterns through rules. Without rules the audit would have nothing to say.

Three things follow from that:

  • Demeanor ships with a large built-in set covering the common .NET frameworks and serialization stacks. The set grows over releases; you don’t have to author anything to get value from the audit.
  • Your codebase has patterns Demeanor doesn’t know about — a homegrown plugin contract, an internal RPC layer, a convention your team enforces. You write rules for those once, and the audit applies them on every subsequent run.
  • Some decisions belong to you, not Demeanor. A method might be safely renameable in your project but not in someone else’s. The rule store has a way to record that choice so it sticks.

The three layers

Rules are resolved from three layers. A rule in a higher layer shadows a same-id rule from a lower layer, and keeps that rule’s position in the catalog — overriding a built-in rule replaces it in place rather than moving it to the end.

LayerWhere it lives on diskTracked in git?Affects the obfuscated output?
Built-inInside Demeanor itselfNo — shipped with the toolYes
Project<project>/.demeanor/patterns/*.jsonYes — this is the layer that travels with the codeYes
SessionIn memory, for the current assistant session onlyNo — discarded when the session endsYes, in that session only

For most teams, only the project layer matters. The built-in layer covers the common cases without configuration. Project rules are the durable artifact.

What Demeanor reads, and from where

Everything the obfuscator consults for a given build lives in one directory at your project root:

your-repo/
  .demeanor/
    patterns/          ← project rules, one or more .json files
      plugin-contracts.json
      rpc-surface.json
    decisions.json     ← needs-decision answers, keyed by rule + type + member

Both are plain JSON, both are meant to be committed, and both are read on every run — from the CLI, from your build, and from an assistant session alike. Demeanor finds them by walking up from the assembly it is obfuscating until it sees a .demeanor directory; the MSBuild integration passes the project directory explicitly, which matters when the built assembly lands in a staging or publish folder away from the source tree. Pass --project <dir> to point it somewhere specific.

If a rule file fails validation it is reported by file name, rule id and reason — never skipped quietly. A rule you believe is protecting something, that isn’t, is worse than no rule at all.

The built-in layer — 26 years of refinement

The built-in layer covers ASP.NET Core, EF Core, Blazor, WPF, WinForms, WCF, COM interop, System.Text.Json, and Newtonsoft.Json out of the box. The catalog has been refined across hundreds of customer codebases over Demeanor’s 26-year history — most of the rules in it exist because a real reflection or serialization pattern broke a real build at some point, and the lesson got encoded. New patterns are added with every release.

You don’t see the built-in rules directly — every decision Demeanor makes during obfuscation is logged in the generated report so you can see the catalog in action, but the catalog definitions themselves are a core proprietary asset. If a built-in rule’s behaviour doesn’t fit your project, the project layer shadows it by id.

Three severities

Every rule carries a severity that tells the audit what to do when it matches.

Auto-protect — act silently

The match is obviously load-bearing. Demeanor keeps the name and reports the count in the “auto-protected” section of the audit. No prompt, no decision required. This is the right severity when the consequence of getting it wrong is the application failing at runtime, and the rule has no plausible exceptions.

Needs decision — protect, and ask

The match looks load-bearing but the audit isn’t certain. It lists the finding under “needs your decision” with a recommendation, and keeps the name until you answer. Silence is not consent: a rule reaches this severity because renaming the match might break the application at run time, and with nobody available to say otherwise, leaving it alone is the only safe answer. Your answer is recorded in .demeanor/decisions.json, committed with your code, and applied on every later run — including CI.

This is the right severity when the rule could have legitimate exceptions, or when the right answer depends on something Demeanor can’t see — a deployment topology, a versioning policy, a private API surface.

Informational — mention it, move on

The obfuscation itself would tolerate the pattern, but it’s worth knowing about — a reflection-path call that won’t survive Native AOT, a public DTO that isn’t registered with the source-generated JSON context, a likely-future-pain shape. No action required to obfuscate; acting on the advisory improves the codebase.

See Exclusions Guide — how the audit classifies findings for how these severities appear in the report. The same taxonomy applies whether the rule came from the built-in layer or your project.

What a project rule looks like

Project rules are JSON files under .demeanor/patterns/ inside your repo. The shape is small enough to read in one glance:

{
  "id": "myteam-plugin-contract",
  "version": 1,
  "predicate-vocab": 1,
  "kind": "type-protection",
  "summary": "Plugin contracts must keep their public surface",
  "reasoning": "Plugins are loaded by reflection from third-party DLLs outside this repo. Renaming the interface members breaks downstream integrators that bind by name.",
  "severity": "auto-protect",
  "provenance": "project",
  "license": "any",
  "when": { "predicate": "implements-interface", "args": "IPlugin" },
  "then": { "freeze": "type-and-members", "report-as": "myteam-plugin-contract" }
}

The fields:

  • id — A stable identifier you choose. Use a prefix (your team or module name) so the rule can’t collide with one Demeanor ships. A rule in your project with the same id as one Demeanor ships will shadow it.
  • summary — A one-line description that shows up in the audit output and in PR review.
  • reasoning — Why this rule exists. This is what your team reads in a year when they’re trying to decide whether to remove it.
  • remediation — What to do about a match. Required for needs-decision and informational rules, since both ask a person to act; an auto-protect rule has already done the work, so it’s optional there.
  • severity — One of auto-protect (protect silently), needs-decision (protect the match and ask), or informational (report the finding, never freeze).
  • when — What the rule matches on, as a predicate. The supported predicates — interface implementation, attribute presence, base type, IL-flow patterns, and more — are documented in Authoring a rule by hand, which is also the working reference for the full JSON schema, the predicate vocabulary, and composition with all / any / not.
  • then — What happens to a match. freeze takes one of seven actions — type-and-members is the common one; report is what an informational rule uses. report-as must equal the rule’s own id.
  • version, predicate-vocab, kind, provenance, license — bookkeeping. The values above are the right ones for a hand-authored project rule.

Rules are validated when they load. A rule that doesn’t match the schema is rejected with a message naming the file, the rule id and what was wrong — it is never silently skipped, because a rule you believe is protecting something and isn’t is worse than no rule at all.

A project usually ends up with somewhere between zero and a dozen rules — the things that are specific to your code. The built-in layer covers the rest.

Where they go

Into .demeanor/patterns/, as described in What Demeanor reads, and from where above. One file per rule, or one file per logical group — whichever your team finds easier to review. Everything in that directory is picked up on every run; only the top level is read, so subdirectories are ignored.

Two ways rules get into your project

1. You write them by hand

Create a JSON file under .demeanor/patterns/, fill in the fields, commit it. The audit picks it up on the next run. This is the right path when you already know the rule you need — a known convention your team enforces, a known fragile interface, a known internal API surface. See Authoring a rule by hand for the full schema, the predicate vocabulary, and worked examples.

2. The conversational workflow proposes them

If you drive the audit through an MCP-capable assistant (Claude Code, Claude Desktop, Cursor, Windsurf, Continue.dev, a VS Code MCP extension), and the assistant encounters a needs-decision finding more than once in a session, it can offer to capture your decision as a project rule. You see the JSON before it’s written, you approve, the file lands in the repo, and the next audit run treats the pattern as resolved — without re-running the conversation.

Both paths produce the same artifact: a JSON file under .demeanor/patterns/ that lives in your repo. The first path is mechanical; the second is collaborative. Neither is required — a project with no rule files still gets the full built-in audit.

Requires your own Claude subscription — not included with Demeanor. Any other MCP-capable assistant drives the same workflow with its own subscription.

Rules vs. [Obfuscation] attributes

Demeanor has had source-level [Obfuscation] attributes since the beginning, and they’re still the right tool for pinning one specific type or member. Rules are the engine-driven layer over them.

[Obfuscation] attributeProject rule
ScopeOne type or member at a timeA pattern — matches every type or member it fits
Lives inThe source file, next to the code.demeanor/patterns/ in the repo root
Best for“Keep this type’s names because the caller looks them up”“Keep every type that implements this interface”
Survives refactorsYes — moves with the typeYes — matched by shape, not by name
Visible in PRIn the source diffIn the .demeanor/patterns/ diff

The two mechanisms compose. Use attributes for pinpoint exclusions next to the code that needs them. Use rules when the same protection should apply to a whole pattern. See the Exclusions Guide for the attribute side and the Decisions & CI page for how rules carry your team’s judgements forward.

What CI sees

The build-time obfuscator reads .demeanor/patterns/ exactly like the interactive audit does. The same rules apply in the same order; the only difference is that CI has no human in the loop to answer needs-decision prompts.

That means two things:

  • Auto-protect and informational rules behave identically in CI and locally. Auto-protect rules act silently; informational rules log their finding and the build proceeds.
  • Needs-decision rules you have resolved are applied in CI exactly as you decided them. Resolutions live in .demeanor/decisions.json, which is committed alongside your rules and read on every build.
  • A needs-decision match nobody has resolved leaves that symbol unrenamed, and says so. There is no human to ask during a build, so Demeanor takes the safe answer and lists what it did. That protects the app, but it is not free — each listed symbol shipped with its original name. Resolve them, or set <DemeanorFailOnPendingDecisions>true</DemeanorFailOnPendingDecisions> to make an unresolved decision fail the build instead.

A clean PR ends with no needs-decision rules firing on CI. See Decisions & CI for how the conversational workflow gets you to that state.

Next steps

  • Authoring a rule by hand — the working reference: full JSON schema, the predicate vocabulary, composition with all / any / not, and worked examples
  • Decisions & CI — how the conversational workflow promotes decisions into project rules, and how CI runs against them with no AI in the loop
  • Exclusions Guide[Obfuscation] attributes and CLI exclusions for pinpoint cases
  • Conversational walkthrough — a real audit session, end to end
  • Getting Started — the canonical CLI guide