Configuration: one source of truth

Where every DefenseClaw setting lives. config.yaml holds what an administrator decided; hook files and provider files are derived from it; locks and ledgers are evidence; approvals, discovery and quarantine are runtime state. Includes the single config writer, policy generations, and the rules for managed hosts.

DefenseClaw keeps every decision an administrator makes in one place: ~/.defenseclaw/config.yaml, plus the few policy assets that file names by digest (a custom rule pack, a scanner policy file, extra YARA rules, an AI-discovery signature pack). Nothing else stores a decision. The gateway, the hooks, the TUI, the CLI and the enterprise lifecycle all read that one file, and every change goes through one writer.

This page explains the model. For the key-by-key schema, see the configuration reference.

Four kinds of state

Every file DefenseClaw keeps is one of four kinds. The kind tells you whether you may edit it, what happens if you do, and how to fix it.

KindWhat it isExamplesIf it is wrong
IntentWhat an administrator decided. The only input to policy.config.yaml, and the assets it pins by digestChange it with defenseclaw config set, setup, the TUI, or by hand on a per-user install
DerivedRendered from intent. Regenerated whenever its inputs change, and never read back as input.Hook scripts, custom-providers.json, enterprise machine policy, composed rule packsDo not edit it. Doctor reports the drift, and doctor --fix, setup or ensure regenerates it
EvidenceA record of what was applied or decided, so that tools can verify it later.config.generation.json, hook_contract_lock.json, migration-v9.json, deployment records, ledgersNot policy. Editing it makes verification fail
Runtime stateWhat DefenseClaw observed or did while running.Approvals, discovery inventory, quarantine, the enforcement journalReported by status and doctor. Never treated as policy

The split removes a class of bugs where two files disagreed. A block list in a database, a thresholds file next to the rules, or a hook script with a baked-in setting could each override what config.yaml said. In config_version: 9 they cannot: if a value changes what DefenseClaw decides, it is in config.yaml.

How a change becomes enforcement

asks
writes
records
reloads
renders
serves
Change intentCLI · TUI · admin push
Config writerlock · validate · record
config.yamlthe one source of truth
New policy generationcompose · compile · digest
Hooks, proxy, watcherread one generation
Derived fileshook scripts · providers
Evidencegeneration record

List view for small screens. Use the expand button to open the drawing.

  1. Change intentCLI · TUI · admin push
    • asksConfig writer
  2. Config writerlock · validate · record
    • writesconfig.yaml
    • recordsEvidence
  3. Evidencegeneration record
  4. config.yamlthe one source of truth
    • reloadsNew policy generation
    • rendersDerived files
  5. Derived fileshook scripts · providers
  6. New policy generationcompose · compile · digest
    • servesHooks, proxy, watcher
  7. Hooks, proxy, watcherread one generation
A change is validated and recorded by the config writer, written to config.yaml, and picked up by the running gateway as a new policy generation. Derived files are rendered from config.yaml; they are never read back.

Three parts of that path matter day to day.

  1. One writer. defenseclaw config set, every defenseclaw setup command, the TUI, defenseclaw skill block and the other block commands, and the gateway's own API all change config.yaml through the same writer. It takes the lock config.yaml.lock (the Go gateway and the Python CLI share it), validates the new file against the schema before it writes anything, replaces the file atomically, and records the write in config.generation.json. A change that fails validation leaves config.yaml untouched.
  2. One generation. The gateway watches config.yaml and the assets it names. On a change it builds a complete new policy generation off the hot path: it composes the rule packs, compiles admission:, resolves the thresholds for every connector and profile, and prepares the policy engine. Then it swaps the generation in atomically. Every hook decision, proxy decision and install-watcher decision reads one generation, so a request never sees half of a change.
  3. A bad change is not applied. If the new generation does not build (a custom rule pack whose digest changed, for example), the gateway keeps enforcing the previous generation and reports the error as last_reload_error. Doctor shows it as a failed Policy check.

Most keys apply with no restart. The keys that name a file or listener the gateway opened at start, or that it reads once at start, still need defenseclaw-gateway restart. A change to one is saved, and the gateway keeps the old value until you restart it:

  • data_dir, observability.local.path and observability.local.judge_bodies_path
  • gateway.* (the listener, authentication and upstream keys), except gateway.watcher.* and the gateway.config_reload keys other than mode
  • guardrail.host, guardrail.port, guardrail.enabled, guardrail.connector, guardrail.scanner_mode and guardrail.retain_judge_bodies
  • the hook self-heal settings (guardrail.hook_self_heal and guardrail.hook_self_heal_debounce_ms)
  • claw, agent, routing and plugin_dir
  • deployment_mode, enterprise.profile and enterprise.network
  • the resource-identity keys (environment, tenant_id, workspace_id, discovery_source)

The storage paths and deployment_mode can not be kept that way: after a change to one, the gateway refuses that reload, and every later one, until you restart it, and /health shows the error as last_reload_error.

Adding, removing, enabling or disabling a connector (guardrail.connectors.<c>.enabled) applies in place: the gateway sets up or removes that connector's hooks and restarts its install watcher, without a process restart. On Secure Client, connector changes still need a restart.

Restart-required keys is the reference list. When your change touches one, config set says so:

Set gateway.port (config generation 9, sha256 b0683fad4394).
Effective policy digest: sha256:c770dec74b1d9a5631df0a0f97d907259a661f6baded49b362722e043e12b76e
Restart the gateway to apply gateway.port: defenseclaw-gateway restart

What lives in config.yaml

config.yaml is administrator intent. These sections decide behaviour:

SectionDecides
guardrailMode, thresholds (block_at, alert_at), rule pack and guardrail.rules, per-connector and per-profile overrides, hook fail mode
admissionWhat happens to a skill, MCP server or plugin by scan severity
asset_policyThe block and allow lists, and registry and runtime-detection rules
scanners, llm, llm_providersScanner policy and gate, the judge model, custom LLM providers
updateUpdate check, channel and download source
observability, webhooks, notificationsWhere telemetry and alerts go, and redaction
enterprise, managed, deployment_modeThe managed-device profile

Read a value, and where it comes from, with config get --effective:

defenseclaw config set guardrail.block_at HIGH
defenseclaw config get guardrail.block_at --effective
HIGH
(source: config:guardrail.block_at)

When you do not set a threshold, the same command prints the rule pack's default and names the pack as the source. The same is true for the admission actions, which derive from the scanner gate until you set them.

Some settings point at a file of their own: a custom rule pack, a scanner policy, extra YARA rules. Those assets are pinned. The config entry carries the file's sha256 digest, so a changed file never takes effect silently:

guardrail:
  rule_pack: acme-hardened
  custom_packs:
    acme-hardened:
      path: /etc/acme/defenseclaw/packs/acme-hardened
      digest: sha256:<64 hex digits from the files digest line of validate-pack>

defenseclaw guardrail validate-pack PATH prints the files digest: line to paste after sha256:. If the digest does not match, the change does not build into a generation, and the previous policy keeps running. See Rules.

On a per-user install you may also edit config.yaml by hand. The running gateway validates the file, applies it, and records the edit in config.generation.json with the actor hand-edit:<os-user>. A hand edit that fails validation is not applied. On a managed host, a hand edit is reverted (see Managed hosts).

What is derived

Derived files are rendered from config.yaml and are not read back as configuration input. Connectors execute generated hook scripts, however: an edit can affect the next hook event before repair. Doctor detects the drift; regenerate the affected file as shown below.

Derived fileRendered fromDrift shows asRegenerate with
Hook scripts, ~/.defenseclaw/hooks/<connector>-hook.sh, and the Windows .hookcfgguardrail.hook_fail_mode, the connector set and the gateway address. Each script carries a # defenseclaw-derived: headerDoctor warns stale generated script for the connectordefenseclaw-gateway restart, or defenseclaw setup <connector>
~/.defenseclaw/custom-providers.json marked _derived_fromllm_providersDoctor fails Custom-provider overlay when the derived file was edited by hand or no longer matchesdefenseclaw doctor --fix
Enterprise machine policy (for example the Codex requirements file and the Claude Code managed settings)The administrator config, inside ensureenterprise policy show and status report driftensure or repair
Composed rule packs, compiled admission:, the threshold tableguardrail.rules, rule_pack, admission, block_at, alert_atNot on disk. They live in the generation and are rebuilt on every reloadNothing to do

A legacy 0.8.x provider overlay is imported during v9 migration when its fields fit llm_providers. If it contains request_overrides, migration leaves the unmarked file as a live input; see Custom providers. It is not a derived file.

Older releases wrote composed protection packs to ~/.defenseclaw/policies/guardrail/protected-<scope>/, and policy YAML to policies/rego/data.json. Neither exists in config_version: 9. The migration converts them into guardrail.rules and admission:; a leftover data.json is ignored, and doctor reports it as Retired policy data. Migrate config.yaml from v8 to v9 lists everything that moved.

What is evidence

Evidence records what was applied. Tools compare the live state with it. Evidence is not policy; changing it does not change what DefenseClaw decides, but it does make a verification step fail.

EvidenceRecordsChecked by
config.generation.json, next to config.yamlThe config generation counter, the sha256 of the file the writer last wrote, the actor, the reason and the timeDoctor warns when the file on disk is not the one recorded (a hand edit)
hook_contract_lock.jsonThe hook contract each connector's setup renderedDoctor and setup
migration-v9.json, config.yaml.v8.bak, data.json.migrated-v9Every value the v9 migration moved, every conflict, and the pre-migration filesDoctor shows Config migration until you run defenseclaw config migrate --ack
committed-config.yaml, rejected-config.yaml, deployment.json, policy-state.json and the guardian ledgers (managed hosts)The config the lifecycle last applied, a rejected in-place edit, the deployment, the policy digest the lifecycle last applied, and the protected targetsstatus and verify (see Lifecycle)
The audit logA config.change.applied event for every write, a config.reload.rejected event when a generation fails to build, and every decision, each stamped with the policy generation and digestThe observability sinks

What is runtime state

Runtime state is what the product saw or did. status and doctor report it, and it is never policy: removing a quarantine record does not change a block list, and a block list does not rewrite a quarantine record.

  • Approvals granted at a prompt (HITL).
  • The AI-discovery inventory (AI discovery).
  • Quarantine records and the quarantined files.
  • The enforcement journal in audit.db: the quarantine and runtime-disable rows the install watcher writes, and its automatic blocks. Only asset_policy blocks and allows an asset by name; the journal records what happened to it.
  • The hook guardian's target list, targets.yaml, on managed hosts.

The effective policy digest

Every policy generation has an effective policy digest: sha256: plus the hash of the canonical form of the migrated config, with secret values removed, paths under data_dir rewritten to a token, defaults filled in, and the digests of every pinned asset and composed rule pack. Managed hosts of one OS that run the same release and received the same admin config report the same digest, which is what makes a fleet comparable. Do not compare across operating systems: the config names different paths on each.

The digest and the generation appear in these places:

WhereWhat you see
defenseclaw status and the TUI headerThe generation, and the first 12 hex digits of the digest
defenseclaw doctorThe Policy row: generation N, digest sha256:abcd... (applied by the gateway)
Gateway /health and /statusA policy object with effective_digest, generation, config_generation, config_generation_recorded, built_at, last_reload_error and per-component digests
Every decision eventdefenseclaw.policy.effective_digest and defenseclaw.policy.generation
Enterprise statusThe policy object: the digest the committed config computes to, the config generation, and whether the gateway reports the same digest

The Policy row fails when the gateway applies a different digest than config.yaml and its assets compute to (a stale gateway), or when the last reload was rejected. It warns when config.yaml was edited outside the writer. The fix for a stale gateway is defenseclaw-gateway restart.

The config generation counts writes of the file. The generation counts applied policy builds, so it also advances when a pinned asset changes. Two hosts can differ in either counter and still have the same digest.

Managed hosts

A managed host is a device on the standalone enterprise profile (deployment_mode: managed_enterprise) that an administrator controls through an MDM or a package. The Secure Client profile is out of scope for all of this: nothing described here changes its behaviour.

On a managed host, the administrator config is the only intent, and ensure is the only writer.

pushes
writes
refused, exit 3
TrustedZ0 · Admin and MDM (admin)
PrivilegedZ1 · Lifecycle (root)
ProtectedZ3 · Admin-owned files
UntrustedZ4 · User session (the user)
Admin configMDM or package
ensureapplies the config
config.yamlowned by an admin
Local writersconfig set · setup · block

List view for small screens. Use the expand button to open the drawing.

TrustedZ0 · Admin and MDM (admin)

  1. Admin configMDM or package
    • pushesensureZ1

PrivilegedZ1 · Lifecycle (root)

  1. ensureapplies the config
    • writesconfig.yamlZ3

ProtectedZ3 · Admin-owned files

  1. config.yamlowned by an admin

UntrustedZ4 · User session (the user)

  1. Local writersconfig set · setup · block
    • refused, exit 3config.yamlZ3
On a managed standalone host only the lifecycle (ensure) writes config.yaml. A local writer is refused with exit code 3, so it cannot change policy.

Local writers refuse

Every local command that would change policy refuses on a managed standalone host and changes nothing. The command exits with code 3:

error: This device is managed: change config.yaml in the admin config (MDM or management plane), not on the device

That covers defenseclaw config set and unset, defenseclaw policy activate, the setup commands that save configuration, the TUI Setup panel (read-only on a managed host), and the gateway's /enforce/block and /enforce/allow routes, which answer 403 with managed_device. skill block, mcp block, plugin block and tool block refuse the same way and point you at asset_policy:

This device is managed: add it to asset_policy in the admin config (MDM or management plane)

Reading is unaffected: config get, config show, status, doctor and enterprise policy show all work. Runtime actions are not config writes, so this gate does not apply to them: quarantine, restore, disable and enable act on runtime state. What a standard user may do beyond that is set by the enterprise profile; see Enterprise concepts.

A hand edit of config.yaml does not survive. On Linux and macOS the lifecycle compares the file with the last config it applied, puts the applied config back, and keeps the edit as rejected-config.yaml; status warns and verify fails with config_rejected until a corrected config is pushed. The block and allow entries a user may have left in audit.db before the device was enrolled are ignored, and ensure reports how many.

Environment variables and .env

The environment is a second place where settings could hide, so a managed host narrows it. Every variable DefenseClaw reads has a managed-host policy in the environment variable reference:

PolicyOn a managed hostExamples
allowThe value is usedRuntime paths, credentials, values the hook scripts set for themselves
ignoreThe value is ignored, in the process environment and in .envSecurity opt-outs, debug switches, test fixtures, DEFENSECLAW_REPO, and DEFENSECLAW_DEPLOYMENT_MODE and DEFENSECLAW_ENTERPRISE_PROFILE
tighten_onlyThe value is used only if it makes things stricterDEFENSECLAW_FAIL_MODE, DEFENSECLAW_STRICT_AVAILABILITY, DEFENSECLAW_HOOK_MAX_BODY

A forged DEFENSECLAW_DEPLOYMENT_MODE or DEFENSECLAW_ENTERPRISE_PROFILE cannot turn a managed host into an unmanaged one. Doctor lists every ignored value it finds, by name only, under its active security overrides.

Updates

update.source changes only where release files are downloaded from. The signature check always uses the release identity compiled into DefenseClaw, so a mirror must serve the same signed checksums.txt bundle. Managed hosts do not self-update: defenseclaw upgrade and defenseclaw rollback refuse there, and the lifecycle applies updates. See Lifecycle and the upgrade guide.

Where do I change it?

I want toChangeCommand
Block at a different severityguardrail.block_atdefenseclaw guardrail block-at HIGH, or defenseclaw config set guardrail.block_at HIGH
Turn a rule off or change its severityguardrail.rulesRules
Block or allow a skill, MCP server, plugin or toolasset_policydefenseclaw skill block NAME, defenseclaw mcp block NAME, ...
Quarantine what a scan findsadmissionAdmission
Point at a custom LLM endpointllm_providers.customdefenseclaw setup provider add
See what the gateway enforces nownonedefenseclaw config get KEY --effective, defenseclaw doctor
Move a v8 config to v9all of the abovedefenseclaw config migrate --dry-run