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 Go configuration contract is version 7. Older files may omit config_version or carry a lower value; the gateway migrates those values in memory before validation so an interrupted upgrade can still start. Release migrations persist changes that must become durable. In particular, the v7 upgrade converts a legacy flat OTel exporter into one named otel.destinations[] route without replacing any destinations already in the file. See Upgrade DefenseClaw for the backup, migration, and verification flow.

On-disk layout

config.yaml
audit.db
.env
picked_connector

config.yaml schema

The shape below mirrors the dataclasses in cli/defenseclaw/config.py and the matching Go structs in internal/config/. Every block is optional — defenseclaw setup * writes only the fields it needs and the merge keeps your hand-edits intact.

config_version: 7             # current Go schema; older files are migrated safely

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              # gateway proxy bind host
  port: 18789                  # gateway proxy port (sidecar -> upstream LLM traffic)
  api_port: 18970              # gateway REST API port (hooks + TUI dial this)
  api_bind: ""                 # optional override for the REST API bind address
  config_reload:
    mode: hot                  # hot | restart; omitted means hot

# Named OTel fan-out destinations. The top-level block owns the master switch,
# resource identity, sampler, and emission policy; each destination has an
# independent transport and bounded export queue.
otel:
  enabled: true
  resource:
    attributes:
      service.name: defenseclaw
  traces:
    sampler: always_on
    sampler_arg: "1.0"
  destinations:
    - name: local-observability
      preset: local-otlp
      enabled: true
      protocol: grpc
      endpoint: 127.0.0.1:4317
      tls: { insecure: true }
      traces: { enabled: true }
      metrics: { enabled: true }
      logs: { enabled: true }
    - name: galileo
      preset: galileo
      enabled: true
      protocol: http
      endpoint: https://api.galileo.ai/otel/traces
      batch: { scheduled_delay_ms: 1000 }
      headers:
        Galileo-API-Key: ${GALILEO_API_KEY}
        project: defenseclaw
        logstream: production
      span_filter:
        operations:
          - name: chat
            require_attributes:
              - gen_ai.operation.name
              - gen_ai.provider.name
              - gen_ai.request.model
              - gen_ai.input.messages
              - gen_ai.output.messages
          - name: invoke_agent
            require_attributes:
              - gen_ai.operation.name
              - gen_ai.agent.name
              - gen_ai.provider.name
              - openinference.span.kind
              - gen_ai.input.messages
              - gen_ai.output.messages
          - name: execute_tool
            require_attributes:
              - gen_ai.operation.name
              - gen_ai.tool.name
              - openinference.span.kind
              - gen_ai.tool.call.arguments
              - gen_ai.tool.call.result
              - gen_ai.input.messages
              - gen_ai.output.messages
      traces: { enabled: true }
      metrics: { enabled: false }
      logs: { enabled: false }

# `defenseclaw upgrade` persists legacy flat `otel.protocol` / `otel.endpoint`
# and signal transport blocks as a named destination. Runtime loading also
# performs the same conversion in memory as an interrupted-upgrade fallback.
# Preview or repair the persisted conversion with:
# `defenseclaw setup observability migrate-otel [--apply]`.
# Sampling remains process-wide and cannot differ by destination.

# 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. See docs/GUARDRAIL.md → Custom Header Forwarding.
  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

# Top-level redaction switches; persistent kill-switches are deliberately
# top-level so multi-tenant operators can audit them without grepping the
# guardrail block.
privacy:
  disable_redaction: false     # true bypasses redaction on every sink

# Audit fan-out is a top-level list; each entry is a kind-tagged sink.
# `setup observability add <preset>` and `setup splunk` both write here.
audit_sinks:
  - name: org-splunk
    kind: splunk_hec
    enabled: true
    endpoint: https://splunk.example.com/services/collector
    splunk_hec:
      token_env: SPLUNK_ACCESS_TOKEN
      index: defenseclaw
  - name: local-otlp-logs
    kind: otlp_logs
    enabled: true
    endpoint: 127.0.0.1:4317
    otlp_logs:
      protocol: grpc
      insecure: true

# Notifier webhooks (Slack / PagerDuty / Webex / generic). Distinct from
# audit sinks — these are managed by `defenseclaw setup webhook`.
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

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, edit gateway.host / gateway.port / gateway.api_port in config.yaml to change bind addresses, and 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)
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 hash-checked backup of each file is stored in ~/.defenseclaw/backups/ before any mutation. Teardown (or --disable) restores the backup byte-for-byte; if the file has drifted since the backup, only DefenseClaw-owned entries are surgically removed.

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.