Reference

Configuration

~/.defenseclaw/config.yaml schema, environment variables, on-disk layout, and per-connector source-of-truth files. The single source of truth for "where does this setting live?"

~/.defenseclaw/config.yaml is the single source of truth for operator-owned configuration. Most fields are managed by defenseclaw setup * commands; you can also hand-edit the file. The running gateway watches the active config.yaml, validates the full file on change, and reconciles supported updates without a full process restart. Some storage identity fields still require a real defenseclaw-gateway restart.

The current strict configuration contract is version 8. Existing supported v7 files are converted automatically by the ordinary defenseclaw upgrade transaction. The gateway does not rewrite legacy files at startup and does not run v7 and v8 observability formats in parallel. See Upgrade DefenseClaw for the backup, atomic migration, rollback, and verification flow.

Native Windows uses the packaged layout

The file tree below describes a source or POSIX installation. The native Windows package uses an embedded runtime and defenseclaw-hook.exe; it does not install a .venv or shell hook scripts. See the native Windows path reference before locating or editing Windows state.

On-disk layout

config.yaml
audit.db
.env
doctor_cache.json
picked_connector

Doctor cache

~/.defenseclaw/doctor_cache.json is the latest non-dry-run Doctor snapshot. The Textual TUI reads it for the Overview panel instead of repeating network-intensive probes on every redraw.

{
  "schema_version": 2,
  "captured_at": "2026-04-17T18:21:09Z",
  "mode": "repair",
  "outcome": "failed",
  "exit_code": 1,
  "passed": 12,
  "failed": 0,
  "warned": 1,
  "skipped": 2,
  "summary": {"passed": 12, "failed": 0, "warned": 1, "skipped": 2},
  "checks": [
    {"status": "warn", "label": "Splunk HEC", "detail": "queue depth 4200/5000"}
  ],
  "repair_summary": {
    "planned": 0,
    "applied": 1,
    "failed": 0,
    "blocked": 1,
    "manual": 0,
    "noop": 0,
    "declined": 0,
    "requires_confirmation": 0
  },
  "repairs": [
    {"repair_id": "doctor.gateway.service.reconcile", "state": "blocked"}
  ]
}

The top-level and summary counts retain their health-only meaning. repair_summary and repairs form a separate ledger; a failed or blocked repair can make the aggregate outcome fail without increasing the health failure count.

The CLI publishes the cache with an atomic temporary-file replacement after each non-dry-run Doctor invocation, including nonzero outcomes. doctor --fix --dry-run is read-only and never writes it. The TUI marks snapshots older than 15 minutes stale. A later healthy check can show that live health recovered, but does not erase a cached failed or blocked repair. A missing cache is normal before the first Doctor run.

config.yaml schema

The shape below shows the normal source-authoring surface. Every block is optional, and setup commands preserve comments and unrelated hand edits. Use defenseclaw config reference observability for the generated all-knobs reference instead of copying every default into this file.

config_version: 8             # strict current schema; upgrade migrates supported v7 files

claw:
  mode: claudecode             # single-connector mode; setup aliases select this with --connector.
                               # Set to `multi` automatically when more than one
                               # connector is active (see guardrail.connectors below).
                               # This value is mirrored to the OTel resource attr
                               # `defenseclaw.claw.mode`.

gateway:
  host: 127.0.0.1              # upstream agent gateway host (OpenClaw/ZeptoClaw fleet link)
  port: 18789                  # upstream agent gateway port
  api_port: 18970              # gateway REST API port (hooks + TUI dial this)
  api_bind: ""                 # REST API bind override; normally 127.0.0.1 when empty
  config_reload:
    mode: hot                  # hot | restart; omitted means hot

# One observability graph owns collection, local history, routing, redaction,
# sampling, metrics policy, and every optional destination. Defaults collect
# all logs/traces/metrics, persist collected logs to SQLite, and export every
# capability of an enabled destination unredacted when send/routes are omitted.
# Galileo's omitted preset policy is additionally restricted to the generated
# available galileo-rich-v2 trace-family membership.
observability:
  resource:
    attributes:
      service.name: defenseclaw-gateway
      deployment.environment.name: production
  trace_policy:
    sampler: parentbased_always_on
    semantic_profile: defenseclaw-genai-rich-v1
  metric_policy:
    export_interval_seconds: 60
    temporality: delta
  local:
    path: ~/.defenseclaw/audit.db
    judge_bodies_path: ~/.defenseclaw/judge_bodies.db
    retention_days: 90
  # Omit defaults/buckets for full-fidelity collection. List only deliberate
  # overrides; a route cannot resurrect a signal disabled at collection.
  buckets:
    diagnostic:
      collect: {logs: false, traces: false, metrics: false}
    model.io:
      redaction_profile: content
  redaction_profiles:
    soc:
      extends: sensitive
      detectors: [pii, credentials, secrets]
      field_classes:
        content: detect
        evidence: detect
        path: hash
        credential: remove
  destinations:
    - name: local-observability
      kind: otlp
      protocol: grpc
      endpoint: 127.0.0.1:4317
      network_safety: {allow_private_networks: true}
      # Omitted send/routes means every bucket and all OTLP capabilities:
      # logs, traces, and metrics. Profile resolves per bucket; model.io uses
      # content above and every other omitted bucket inherits none.
    - name: galileo
      kind: otlp
      preset: galileo
      protocol: http/protobuf
      endpoint: https://api.galileo.ai/otel/traces
      batch: { scheduled_delay_ms: 1000 }
      headers:
        Galileo-API-Key: {env: GALILEO_API_KEY}
        project: defenseclaw
        logstream: production
      # Keep send/routes omitted for the compiler-owned traces-only route whose
      # event_names equal the generated available Galileo family membership.
      # Explicit send/routes replace that filter and express operator intent.
    # More optional destinations join the same graph. Splunk HEC is logs-only.
    - name: org-splunk
      kind: splunk_hec
      endpoint: https://splunk.example.com/services/collector/event
      token_env: SPLUNK_ACCESS_TOKEN
      index: defenseclaw
      send:
        signals: [logs]
        buckets: [compliance.activity, security.finding, enforcement.action]
        redaction_profile: soc

# Top-level LLM block. Written by `defenseclaw setup llm` (and copied
# into per-component blocks via the `--inherit-from` machinery). The
# `role` field decides who consumes this block at resolve time:
#   - `unified` (default): every component that does not declare its
#     own `llm:` reads from here.
#   - `agent`: guardrail.judge.llm stays empty and inherits through
#     the unified merge — proxy connectors only.
#   - `judge`: writes guardrail.judge.llm directly, leaves the top
#     level alone — useful for hook-based connectors that already
#     route the agent's traffic elsewhere.
# See `defenseclaw setup guardrail --llm-role judge_only|judge_and_agent`
# for the connector-aware variant.
llm:
  provider: anthropic          # anthropic | openai | bedrock | vertex_ai | azure | custom
  model: claude-sonnet-4-5
  api_key_env: DEFENSECLAW_LLM_KEY
  base_url: ""                 # optional override; LiteLLM picks a default per provider
  role: unified                # unified | agent | judge
  instance_name: ""            # binds to a ~/.defenseclaw/custom-providers.json entry; required with provider: custom
  region: ""                   # generic regional hint (Bedrock / Vertex); provider-specific sub-blocks below take precedence
  # forward_custom_headers controls whether the guardrail gateway
  # forwards inbound HTTP headers from the agent to the upstream LLM
  # provider on both /v1/chat/completions and the passthrough path
  # (/v1/responses, /v1/messages, Bedrock/Gemini native, ...). Default
  # is on; set to false to suppress all inbound header copying so the
  # upstream only sees the canonical Authorization the gateway re-mints
  # from the secrets sidecar. A small blocklist (proxy-hop, auth, host,
  # X-DC-*, X-DefenseClaw-*, W3C trace context) plus RFC 7230 /
  # printable-ASCII validation and 64-header / 32 KiB caps apply
  # regardless.
  forward_custom_headers: true

  # Provider-specific sub-blocks. Only the one matching `provider`
  # is consulted; the others may exist as leftover state from a
  # previous setup and are ignored at resolve time.
  bedrock:
    region: us-east-1
    auth_mode: iam_credentials # api_key | iam_credentials | profile | instance_role
    access_key_env: AWS_ACCESS_KEY_ID
    secret_key_env: AWS_SECRET_ACCESS_KEY
    session_token_env: AWS_SESSION_TOKEN
    profile_name: ""
    inference_profile: us.     # optional model-id prefix
    deployment_aliases: {}     # alias -> model-id, populated by --bedrock-deployment

  vertex:
    project_id: acme-prod-vertex
    region: us-central1
    auth_mode: service_account # service_account | adc | workload_identity
    service_account_json_env: GOOGLE_APPLICATION_CREDENTIALS

  azure:
    endpoint: https://my-resource.openai.azure.com
    api_version: 2024-10-21
    auth_mode: api_key         # api_key | managed_identity
    deployment_aliases: {}     # model -> deployment, populated by --azure-deployment-alias

  # Inline TLS posture for self-signed or internal endpoints. Both
  # `ca_cert_pem` and `insecure_skip_verify` exist so the gateway
  # never has to read another file at request time; `doctor` warns
  # when both are set on the same block.
  tls:
    ca_cert_pem: ""            # PEM bundle inlined from --tls-ca-cert-file
    insecure_skip_verify: false

guardrail:
  enabled: true
  connector: claudecode        # actively enforced connector
  mode: action                 # observe | action
  scanner_mode: local          # local | remote | both
  rule_pack_dir: ~/.defenseclaw/policies/guardrail/default  # path; --rule-pack picks the bundled profile dir
  port: 4000                   # guardrail proxy port
  block_message: ""            # custom; empty -> default
  detection_strategy: regex_only  # regex_only | regex_judge | judge_first

  cisco:
    endpoint: ""
    api_key_env: CISCO_AI_DEFENSE_API_KEY
    timeout_ms: 5000

  # When `judge.llm` is omitted, the judge inherits from the top-level
  # `llm:` block. Populate it explicitly to point the judge at a
  # different backend (e.g. an internal Bedrock instance) without
  # changing what the agent talks to.
  judge:
    enabled: false
    model: anthropic/claude-sonnet-4-20250514
    api_base: ""
    api_key_env: DEFENSECLAW_LLM_KEY
    llm: {}                    # same shape as top-level `llm:` above

  hilt:
    enabled: false
    min_severity: HIGH         # stored uppercase; CLI accepts high|medium|low|critical

  # Multi-connector overlay. One gateway can enforce guardrail policy for
  # several hook connectors at once; each key under `connectors` is a
  # connector name (codex, claudecode, antigravity, ...) carrying a
  # subset of the guardrail knobs above. Every field is OPTIONAL and
  # inherits the global `guardrail.*` value when unset. The singular
  # `guardrail.connector` above keeps working for single-connector
  # installs; this map is purely additive. Proxy connectors (openclaw,
  # zeptoclaw) cannot appear here — multi-connector is hook-only.
  # Managed by `defenseclaw setup <connector>` (choosing "Add") and the
  # `defenseclaw guardrail ... --connector X` command group.
  connectors:
    codex:
      enabled: true            # pointer field: omit = inherit (enabled); false = explicitly off
      mode: action             # observe | action; inherits guardrail.mode when unset
      hook_fail_mode: closed   # open | closed; inherits guardrail.hook_fail_mode
      block_message: ""        # custom; inherits guardrail.block_message
      rule_pack_dir: ~/.defenseclaw/policies/guardrail/strict  # inherits guardrail.rule_pack_dir
      hilt:
        enabled: true
        min_severity: HIGH
    claudecode:
      mode: observe            # only logs; everything else inherits the global default

# Notifier webhooks remain separate from telemetry routing.
webhooks:
  - name: oncall-slack
    type: slack
    enabled: true
    url: https://hooks.slack.com/services/T000/B000/XXX
    secret_env: ""             # optional HMAC for `type: generic`
    min_severity: HIGH

When gateway.api_bind is empty, the sidecar uses 127.0.0.1 except in standalone OpenShell mode: if guardrail.host is non-empty and is not the literal localhost, the management API inherits that host. Pin gateway.api_bind: 127.0.0.1 when the API must stay loopback-only. See Gateway API: bind address.

The source file stays small because omissions compile to explicit effective policy. With observability: {}, all 14 buckets collect logs, traces, and metrics, local SQLite stores all collected logs unredacted, and no remote destination exists. Adding an enabled destination without send or routes selects every bucket, every signal supported by that destination, and profile none (unredacted). A logs-only kind therefore sends all logs; a general OTLP kind sends logs, traces, and metrics. The Galileo preset instead generates one traces-only capability-default route whose event-name selector is restricted to the available family membership in its generated compatibility profile.

An explicit send block or advanced routes block replaces that generated route. For Galileo this is operator intent, not an implicit intersection with profile membership. Explicit policy can narrow buckets and select a destination-specific redaction profile, but any selected nonmember reaches the compatibility projector, is rejected as unsupported_shape, and is reported through destination failure accounting and health. Enumerate reviewed compatible event_names in an advanced route when exact family control is required; inspect the generated membership and effective route with defenseclaw config show --effective --section observability and defenseclaw observability plan before activation.

Multiple destinations are independent fan-out legs. The same collected record can go to SQLite, Splunk, local OTLP, and Galileo, with a different selector and redaction profile on each leg. An optional destination failure does not disable another leg.

For advanced routes, YAML order matters: the first matching route wins for that destination and signal, and an unmatched record is not delivered. Selector fields are ANDed; values within one field are ORed. Supported selectors are buckets, sources, connectors, actions, event_names, and min_severity. Collection happens first, so routing cannot recreate a disabled signal.

Exactly one generated local SQLite destination is mandatory. It is configured only through observability.local, not listed under destinations, and cannot be disabled or filtered. retention_days: 0 means retain forever and produces a capacity warning. guardrail.retain_judge_bodies independently controls whether new raw judge bodies are captured.

Destination transport and queue limits

At most 64 destinations may be configured, with names unique after canonical normalization. One advanced routes list has at most 256 entries. Queue-backed destinations default to 2,048 projected records and 67,108,864 bytes; valid bounds are 1..65536 records and 4198400..268435456 bytes. JSONL and console accept only these queue fields.

Splunk HEC, HTTP JSONL, and OTLP additionally default to batches of at most 512 records/8,388,608 fully encoded bytes with a 5,000 ms scheduled delay and a 10,000 ms per-attempt timeout. Batch count is 1..8192 and cannot exceed queue count; batch bytes are 4263936..67108864; delay is 1..600000 ms. Galileo's omitted preset delay resolves to 1,000 ms. Prometheus is pull-based and rejects batch.

If queue count or bytes would overflow, the newest attempted enqueue is dropped; older FIFO work, mandatory SQLite, and sibling destinations remain intact. Transient or ambiguous acknowledgements retry the exact immutable projection and record ID. Remote delivery is not exactly once because an acknowledgement can be lost after receipt. The effective plan shows all resolved adapter/preset defaults.

See Observability for the bucket catalog, capability matrix, route examples, and redaction precedence.

gateway.config_reload.mode controls what happens after a valid config.yaml change:

ModeBehavior
hotDefault. Reload, validate, diff, and reconcile in the running gateway. Simple guardrail settings are hot-applied; affected in-process loops are restarted only when needed.
restartValidate first, then use a fresh gateway process for substantive config edits. Built-in daemon mode launches the normal defenseclaw-gateway restart path; service-supervised foreground runs exit cleanly for the supervisor to restart. Changing only this mode arms the behavior and does not immediately restart.

Live reload rejects storage identity changes such as data_dir, audit DB path, judge bodies DB path, and gateway.device_key_file unless restart mode is enabled or the operator restarts the gateway manually.

The full LLM configuration story — picking a role, binding a custom-provider instance, configuring regional Bedrock / Vertex / Azure backends, and how defenseclaw doctor validates each — lives on Setup → Unified LLM key. The schema above is the on-disk shape; the page is the operator-facing how-to.

guardrail.connectors and claw.mode: multi

A single gateway can enforce guardrail policy for several hook connectors at once. Each connector that's active gets a guardrail.connectors.<name> block; the gateway resolves policy per connector and falls back to the global guardrail.* values for anything a block leaves unset.

Per-connector keyTypeInherits when unset
enabledbool pointer — omit = inherit (on); false = explicitly off (drops it from the active set, removes its hooks)on
modeobserve | actionguardrail.mode
hook_fail_modeopen | closedguardrail.hook_fail_mode
block_messagestringguardrail.block_message
rule_pack_dirpathguardrail.rule_pack_dir
hilt{ enabled, min_severity }guardrail.hilt

claw.mode becomes multi automatically once more than one connector is active. That sentinel is mirrored onto the OTel resource attribute defenseclaw.claw.mode, so a fan-out gateway is distinguishable from a single-connector one in dashboards and SIEM. The singular guardrail.connector is untouched and still drives single-connector installs — the map is purely additive. Proxy connectors (OpenClaw, ZeptoClaw) cannot be entries here; multi-connector is hook-only. Manage these blocks with defenseclaw setup <connector> (choosing Add) and the defenseclaw guardrail ... --connector X command group — see Setup → Multi-connector.

Custom-provider overlay (~/.defenseclaw/custom-providers.json)

Custom-provider instances live in a separate JSON overlay so the same instance definition can be shared across roles (agent, judge) and across hosts. The Python merger (_apply_instance_overlay) and the Go dispatcher (buildProviderFromEffective) both apply the same rule: the role wins; the overlay fills blanks.

{
  "providers": [
    {
      "name": "acme-internal-bedrock",
      "base_provider_type": "bedrock",
      "base_url": "https://llm.internal:8443",
      "domains": ["llm.internal"],
      "env_key": "ACME_BEDROCK_KEY",
      "allowed_request_types": ["chat", "embedding"],
      "available_models": ["us.anthropic.claude-sonnet-4-6"],
      "request_path_overrides": {
        "chat": "/openai/v1/chat/completions"
      },

      "tls": {
        "ca_cert_pem": "-----BEGIN CERTIFICATE-----\n...",
        "insecure_skip_verify": false
      },

      "bedrock": {
        "region": "us-east-1",
        "auth_mode": "iam_credentials",
        "access_key_env": "AWS_ACCESS_KEY_ID",
        "secret_key_env": "AWS_SECRET_ACCESS_KEY",
        "session_token_env": "AWS_SESSION_TOKEN",
        "profile_name": "",
        "inference_profile": "us.",
        "deployment_aliases": {
          "fast": "anthropic.claude-3-haiku-20240307-v1:0"
        }
      },

      "vertex": {
        "project_id": "acme-prod-vertex",
        "region": "us-central1",
        "auth_mode": "service_account",
        "service_account_json_env": "GOOGLE_APPLICATION_CREDENTIALS"
      },

      "azure": {
        "endpoint": "https://my-resource.openai.azure.com",
        "api_version": "2024-10-21",
        "auth_mode": "api_key",
        "deployment_aliases": {
          "gpt-4o": "prod-gpt4o-eus"
        }
      }
    }
  ]
}

In practice an entry only carries the sub-block matching its base_provider_type — extras are ignored at dispatch time but defenseclaw doctor warns about family mismatches (e.g. bedrock block with base_provider_type: openai), unknown auth_mode values, and dead overlay fields the role-level config already shadows. Auth modes are the same as on setup llm (api_key / iam_credentials / profile / instance_role for Bedrock; service_account / adc / workload_identity for Vertex; api_key / managed_identity for Azure).

The domains array drives the gateway's URL → overlay lookup: when an inbound request URL (set on X-DC-Target-URL by fetch-interceptor agents, or recorded in the connector snapshot for native binaries) matches one of the listed hosts, the resolver applies this overlay entry's TLS, base_url, and sub-block posture. Any entry that declares base_url should also list the matching host in domains; defenseclaw doctor warns when the two diverge.

Environment variables

A handful of high-traffic env vars are inlined below. The full inventory — every variable the CLI and gateway read, with file:line references — lives on Reference → Environment variables.

Prop

Type

There is no DEFENSECLAW_GATEWAY_BIND, DEFENSECLAW_DATA_DIR, or DEFENSECLAW_LOG_LEVEL env var today. Use DEFENSECLAW_HOME to relocate the data directory. Configure the sidecar API listener with gateway.api_bind and gateway.api_port, and the separate guardrail proxy listener with guardrail.host and guardrail.port; gateway.host/gateway.port are the upstream agent-gateway address. Set log verbosity through the sidecar's --log-level flag (see defenseclaw-gateway start --help).

Per-connector source-of-truth files

ConnectorFile DefenseClaw mutates
OpenClaw~/.openclaw/openclaw.json, ~/.openclaw/extensions/defenseclaw/
ZeptoClaw~/.zeptoclaw/config.json
Claude Code~/.claude/settings.json
Codex~/.codex/config.toml
Cursor~/.cursor/hooks.json
Windsurf~/.codeium/windsurf/hooks.json
Gemini CLI~/.gemini/settings.json
GitHub Copilot CLI~/.copilot/hooks/defenseclaw.json by default; <workspace>/.github/hooks/defenseclaw.json with --workspace
OpenHands~/.openhands/hooks.json by default; <workspace>/.openhands/hooks.json with --workspace
Antigravity~/.gemini/config/hooks.json (global only — the path agy v1.0.x actually evaluates; the marketing-facing ~/.gemini/antigravity-cli/hooks.json is silently ignored at runtime; agy merges all discovered hooks files, so DefenseClaw never patches workspace-local copies)
Hermes~/.hermes/config.yaml
OpenCode~/.config/opencode/plugins/defenseclaw.js (managed bridge plugin — written whole, not patched; removed on teardown)
Amp~/.config/amp/plugins/defenseclaw.ts on macOS/Linux or %USERPROFILE%\.config\amp\plugins\defenseclaw.ts on native Windows (managed system policy plugin)
OmniGent$OMNIGENT_CONFIG_HOME/config.yaml when set (otherwise ~/.omnigent/config.yaml), ~/.defenseclaw/hooks/defenseclaw_omnigent_policy.py, and defenseclaw_omnigent.pth in OmniGent's Python environment

A managed backup record is stored at ~/.defenseclaw/connector_backups/<connector>/<logical-name>.json before the first mutation. The JSON record binds the connector, logical name, and absolute target path and stores the pristine bytes, permissions, pristine hash, and post-setup hash. Teardown (or --disable) restores the pristine bytes only when the current file still matches the recorded managed-file identity. If the file has drifted, connector-specific teardown removes only DefenseClaw-owned entries and preserves unrelated operator edits.

Hook connectors default to global/user scope. claw.workspace_dir is empty unless you pass --workspace; when set, OpenHands uses it for repo-local .openhands/hooks.json, .agents/skills, and deprecated .openhands/skills discovery while still scanning global user skills and the OpenHands public skills cache. Copilot uses it for .github/hooks/defenseclaw.json and workspace-local component discovery. Re-run defenseclaw setup <connector> without --workspace to return to global scope.

Every hook setup also writes the resolved connector path contract to ~/.defenseclaw/hook_contract_lock.json. The locations block records the pinned workspace, hook config file, generated hook script, and the MCP/skills/rules/plugins/agents surfaces that DefenseClaw will scan for that connector. Use defenseclaw doctor to compare that lock against the files the active SDK can actually load.

Reload without restart

defenseclaw-gateway policy reload

Tells the running sidecar to re-read OPA policies from disk without bouncing the daemon. Connector wiring (hook scripts, agent files) is not re-applied — use defenseclaw setup guardrail --restart for that. Note this is on the Go sidecar binary (defenseclaw-gateway), not the Python CLI; the Python CLI has no top-level gateway group.