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
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: HIGHWhen 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:
| Mode | Behavior |
|---|---|
hot | Default. Reload, validate, diff, and reconcile in the running gateway. Simple guardrail settings are hot-applied; affected in-process loops are restarted only when needed. |
restart | Validate 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 key | Type | Inherits when unset |
|---|---|---|
enabled | bool pointer — omit = inherit (on); false = explicitly off (drops it from the active set, removes its hooks) | on |
mode | observe | action | guardrail.mode |
hook_fail_mode | open | closed | guardrail.hook_fail_mode |
block_message | string | guardrail.block_message |
rule_pack_dir | path | guardrail.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
| Connector | File 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 reloadTells 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.
Gateway API
The defenseclaw-gateway sidecar API — its registered route patterns, authentication and CSRF model, and inspection verdict shape. The handler code is authoritative.
Keys
The defenseclaw keys registry, credential resolution order, dynamically discovered credential references, and separately managed gateway tokens.