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
fleet_mode: auto # auto | enabled | disabled: when to dial host:port (see below)
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: 7 # default rolling window; larger values are deliberate overrides
# 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
# Optional semantic model classifier for proxy-mode OpenAI Chat Completions.
# Routing-only edits are validated by config reload but activate on gateway
# restart. Omit `remote` to run the pinned, loopback-only Docker sidecar.
routing:
enabled: false
version: "0.3.0" # the only tested managed-sidecar contract
port: 8080
algorithm: static
# remote: {endpoint: https://router.example.com} # sends prompt content
models:
- name: fast # classifier alias; unique within this catalog
provider: openai
model: gpt-4o-mini
base_url: https://api.openai.com
api_key_env: OPENAI_API_KEY
capabilities: [general]
signals:
keywords:
- name: code-request
keywords: [debug, implement, refactor]
operator: OR
decisions:
- name: code
priority: 100
conditions: [{type: keyword, name: code-request}]
model_refs: [fast]
- name: default # an unconditional decision is the fallback
priority: 1
model_refs: [fast]
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
block_at: "" # CRITICAL | HIGH | MEDIUM | LOW; empty -> the rule pack's level
alert_at: "" # same values; never above block_at
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
block_at: HIGH # inherits guardrail.block_at, then the rule pack's level
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. The only
exception is a host that still carries the removed legacy standalone sandbox
(openshell.mode: standalone with a guardrail.host that is non-empty and not
the literal localhost): the management API inherits guardrail.host until
defenseclaw sandbox legacy-cleanup resets the config. An explicit
gateway.api_bind always wins. See
Gateway API: bind address.
The openshell: block for the NVIDIA OpenShell sandbox is described in
OpenShell sandbox settings below.
OpenClaw fleet link
The gateway dials the OpenClaw or ZeptoClaw gateway at gateway.host and
gateway.port only when there is one to reach. gateway.fleet_mode: enabled
always dials and disabled never does. With auto, the default:
- the
openclawandzeptoclawconnectors dial; codexandclaudecodedial only whengateway.hostis not loopback;- every other connector, and an install with no connector, does not dial.
claw.mode defaults to openclaw, so an install that never picked a
connector (a sandbox-only install, for example) names OpenClaw without having
it. When only claw.mode names OpenClaw (guardrail.connector is empty and
guardrail.connectors has no openclaw entry), gateway.host is loopback,
fleet_mode is unset or auto, and OpenClaw is not installed, the gateway
does not dial. OpenClaw counts as installed when an openclaw.json exists at
claw.config_file or in claw.home_dir, or an openclaw binary is on PATH
or in /usr/local/bin, /opt/homebrew/bin, ~/.npm-global/bin or
~/.local/bin. Instead of reconnecting forever, defenseclaw-gateway status
shows the Gateway subsystem DISABLED with
OpenClaw gateway off (OpenClaw is not installed), doctor reports
OpenClaw gateway: off (OpenClaw is not installed), the TUI and the Mac app
show the same text, and OPENCLAW_GATEWAY_TOKEN is not required. The gateway
checks when it starts, so restart it after installing OpenClaw, or set
fleet_mode: enabled to dial regardless.
Routing and observability behavior
The routing block affects OpenClaw and ZeptoClaw only when they use the proxy's
OpenAI-compatible Chat Completions path. Hook connectors and other provider API
shapes keep their existing path. defenseclaw keys list dynamically discovers
each non-empty api_key_env; the classifier never receives the key value. See
Semantic model routing for the exact lifecycle,
privacy boundary, and compatibility matrix.
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. Omitting retention_days uses the seven-day rolling
default shown above. Set a larger value only when the additional local storage
and retention scope are intentional. 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 only for edits that cross a restart boundary, such as listener, topology, or storage changes. Fields that are explicitly hot-reloadable—including observability destinations—still reconcile in process so active agent sessions are not interrupted. Built-in daemon mode launches the normal defenseclaw-gateway restart path when one is required; 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.
OpenShell sandbox settings
The openshell: block configures NVIDIA OpenShell 0.1 sandboxes, the
defenseclaw sandbox commands. You rarely
edit it by hand. sandbox setup writes enabled, harnesses, and
upstream_telemetry, and sandbox enable and disable keep wrappers in
step with your shell rc files. sandbox policy allow and block edit the
egress lists, and "always" answers to an unblock or an ask are saved to
egress.unblocked or egress.block. See also
Sandbox. Policy packs, how the layers combine, and the
administrator's openshell.admin limits are explained in
Sandbox policy packs and admin controls.
Where sandboxes run
Sandboxes run on Linux and on Apple-silicon Macs, with the DefenseClaw
gateway and the OpenShell gateway both running as your own user. On a Mac
they are OpenShell MicroVMs, which mount no host folders, so
openshell.workdir.mode is always copy there (see
macOS). In a managed_enterprise
install the gateway doesn't start the sandbox runtime yet: /health
reports the sandbox subsystem as disabled, and sandbox setup and
sandbox run refuse to start. The openshell keys, openshell.admin
included, are still validated there.
The posture is layered. The selected sandbox policy pack supplies every
default, the keys below override it, the options of sandbox run override
those, and openshell.admin clamps the result. Keys the pack governs have no
loader default, so an unset key follows the pack: profile, yolo,
workdir.mode, workdir.max_upload_mb, egress.ports,
egress.large_upload_mb, egress.block_large_uploads, egress.feed, and
mcp.import. The mask, unmask,
block, and allow lists add to the pack's lists. Some settings exist only in a
pack, so a custom pack is the way to change them: the approvals mode, the
review list, mcp.blocked_tools, mcp.project_servers (whether the MCP
servers a repository defines may start), and hooks.on_tamper (whether a
sandbox whose hooks were bypassed is stopped or only reported).
openshell:
enabled: false # true starts the sandbox runtime; sandbox setup sets it
binary: openshell # the upstream openshell CLI
gateway:
name: "" # OpenShell gateway registration; "" = the active one
workspace: "" # OpenShell workspace; "" = default
ingress_port: 0 # sandbox hook ingress; 0 = gateway.api_port + 1
egress_port: 0 # DefenseClaw egress proxy; 0 = gateway.api_port + 2
pack: "" # "" = open; a built-in, a name under pack_dir, or an absolute path
pack_dir: ~/.defenseclaw/policies/sandbox # default <data_dir>/policies/sandbox
profile: "" # "" follows the pack; open | balanced | strict
yolo: null # null follows the pack; true | false
llm: auto # model credential a run shares; see below
keep_headless: false # true keeps a --prompt run's sandbox
workdir:
mode: "" # "" follows the pack; mount | copy
masks: [] # secret-file globs added to the pack's masks
unmask: [] # project globs that stay visible despite a mask
max_upload_mb: 0 # copy-mode size limit; 0 follows the pack
git_depth: 200 # history depth of the copy-mode clone
on_exit: ask # ask | keep | undo
undo_ignored:
enabled: false # true: undo restores these directories (Linux mount mode)
max_mb: 500 # cap on one undo point's copies, in MiB
dirs: [node_modules, .venv, venv]
egress:
block: [] # host patterns added to the pack's block list
allow: [] # host patterns added to the allow list
unblocked: [] # "always" unblocks and approvals the daemon saves
ports: [] # replaces the pack's proxy ports when non-empty
large_upload_mb: 0 # first-seen-host upload alert in MiB; 0 follows the pack
block_large_uploads: false # true also cuts that upload; false follows the pack
feed: "" # "" follows the pack; builtin | none
image:
base: "" # "" = the pinned NVIDIA community base image
harness_versions: {} # harness name -> exact version
approvals:
debounce_ms: 3000 # batch approvals until hooks are quiet this long
agent_proposals: null # null = true
resources:
cpu: "" # "" = unlimited
memory: "" # "" = unlimited
harnesses: [] # harnesses set up for sandboxes
wrappers: [] # harnesses with the shell wrapper
mcp:
import: null # null follows the pack
host_ports: [] # host ports opened for host-side MCP servers
upstream_telemetry: false # OpenShell's own anonymous usage telemetry
token_delivery: provider # provider | env
middleware:
enabled: false # reserved; nothing reads it yet
admin: {} # administrator limits; see belowSandbox keys
| Key | Default | Values and rules |
|---|---|---|
enabled | false | The sandbox switch. true starts the sandbox runtime with the API: the hook ingress and egress proxy listeners and the sandbox manager behind the sandbox API. It also turns on the port checks under Validation and reload, and a change restarts the API listener. sandbox setup sets it to true, and sandbox teardown to false. |
binary | openshell | The upstream openshell CLI, used for terminal attach, command runs, file transfer, and provider profile imports. A command name looked up on PATH, or an absolute path. A relative path with a separator, such as bin/openshell or ./openshell, is refused: it would run from the project folder. |
gateway.name | "" | The local OpenShell gateway registration. Empty selects the active registration. At most 128 characters. |
gateway.workspace | "" | The OpenShell workspace (tenant). Empty selects default. At most 128 characters. |
ingress_port | 0 | The sandbox hook ingress port, 0–65535. 0 means gateway.api_port + 1 (18971 by default). Its OpenShell provider profile is defenseclaw-ingress-<port>. Sandboxes keep the port their image and policy were built for, so re-create them after a change. |
egress_port | 0 | The egress proxy port, 0–65535. 0 means gateway.api_port + 2 (18972 by default). |
pack | "" | The sandbox policy pack: open, balanced, strict, a custom pack name under pack_dir, or an absolute path (or ~/…) to a pack.yaml or its directory. Empty selects open. |
pack_dir | <data_dir>/policies/sandbox | Where custom packs live, as <name>/pack.yaml. An absolute path, or one starting with ~/. Empty turns custom pack names off. |
profile | "" | open, balanced, or strict. Empty follows the pack's network mode. |
yolo | null | Skip-permissions mode for the harness. null follows the pack. |
keep_headless | false | true keeps the sandbox a headless sandbox run creates (--prompt, or the harness's print mode, such as the shell wrapper's claude -p), as --keep does for one run. By default it is deleted when the run ends and nothing is left in it to bring back or undo, by the rules of --rm. |
llm | auto | The model credential a run shares with its sandbox: auto (the first key found for the harness, an Amazon Bedrock key last), none, anthropic, claude-oauth, openai, bedrock, or gemini. It is the default of sandbox run --llm, so the runs the shell wrappers, the TUI and the macOS app start take it too; --llm overrides it for one run. A provider the harness has no profile for falls back to auto; one whose key is not set refuses the run. |
workdir.mode | "" | mount (live bind mount) or copy. Empty follows the pack. |
workdir.masks | [] | Project globs of secret files, added to the pack's masks. |
workdir.unmask | [] | Project globs that stay visible even though a mask covers them, added to the pack's own unmask list. |
workdir.max_upload_mb | 0 | Copy-mode size limit in MiB, 0–1048576. 0 follows the pack. |
workdir.git_depth | 200 | History depth of the copy-mode clone, 0–1000000. 0 uses 200. |
workdir.undo_ignored.enabled | false | Whether a mounted project's undo point keeps a copy of the directories in workdir.undo_ignored.dirs that git ignores (in a folder without git, the dependency directories its snapshot skips), so sandbox undo restores them as the session found them. Copies are file clones where the filesystem supports them (btrfs, XFS) and byte copies otherwise (ext4). It applies from the next session's start, on Linux in mount mode; copy mode, every sandbox on a Mac, has no undo point. |
workdir.undo_ignored.max_mb | 500 | The cap on one undo point's copies in MiB, 0–1048576; 0 means 500. A directory whose copy would pass it keeps none, and undo reports it as before. |
workdir.undo_ignored.dirs | [node_modules, .venv, venv] | Directory names kept, matched at any depth: one path segment, not .git, up to 64 names. Empty means the default. |
workdir.on_exit | ask | What happens to a mounted project's changes when a session ends: ask, keep, or undo. Without a terminal, or with --yes, ask keeps them. |
egress.block | [] | Host patterns added to the pack's block list. An unblock can't lift an entry; remove it to reach the host. A "reject always" answer to an ask adds its hosts here. |
egress.allow | [] | Host patterns added to the allow list. An entry exempts the host from the blocklist feed, and one that names a private host (an exact name, an intranet wildcard such as *.corp, or an address) opens it. Under strict, a direct connection to an entry is approved without asking. Entries that cover every host or a whole top-level domain are ignored, and all entries are ignored when admin.allow_unblock is false. |
egress.unblocked | [] | Hosts the daemon saves when you choose "always" for an unblock or an approval. Validated like egress.allow, and ignored when admin.allow_unblock is false. Unlike egress.allow, an entry here lifts only blocklist-feed and allowlist refusals: it never opens a private address the name resolves to. |
egress.ports | [] | Destination ports for the egress proxy, 1–65535, unique, at most 64. A non-empty list replaces the pack's ports. |
egress.large_upload_mb | 0 | Report an upload of more than this many MiB to a host the sandbox hasn't contacted before, 0–1048576. 0 follows each sandbox's pack, including a run's --pack. A change reaches running sandboxes on the next reload. |
egress.block_large_uploads | false | true also cuts that upload before the chunk that crosses the threshold and refuses the sandbox's later requests to the host (and to other new hosts under its domain or at its address), for every sandbox. The feed shows a ✗ with the threshold, and defenseclaw sandbox unblock lifts the block for a host. Hosts on an allow list you or your administrator wrote, and hosts you unblocked, are only reported. false follows each sandbox's pack (egress.block_large_uploads); it can't turn a pack's block off. The block acts on the report, so a sandbox whose pack turns the report off (large_upload_mb: 0) keeps it off, and sandbox policy explain says why. See Large-upload alert threshold. |
egress.feed | "" | builtin turns the built-in blocklist feed on, none turns the feeds off. Empty follows the pack. |
image.base | "" | A bring-your-own base image. The image build refuses a base that isn't pinned by digest (<image>@sha256:<64 hex digits>). Empty selects the pinned NVIDIA OpenShell community base image. |
image.harness_versions | {} | Harness name to exact version, at most 64 entries. The image build refuses a version outside DefenseClaw's reviewed hook contract. |
approvals.debounce_ms | 3000 | 0–600000. How long hooks must be quiet before the sandbox manager applies a batch of approvals: every OpenShell policy reload closes open connections. |
approvals.agent_proposals | null | Whether the agent may submit its own connection requests for triage; false rejects them. null means true. |
resources.cpu | "" | CPU request: cores ("2", "1.5") or millicores ("500m"). Empty means unlimited. sandbox run --cpu overrides it. |
resources.memory | "" | Memory request: bytes with an optional Ki, Mi, Gi, Ti or k, K, M, G, T suffix ("512Mi", "4Gi"). Empty means unlimited. sandbox run --memory overrides it. |
harnesses | [] | The harnesses set up for sandboxes, such as claudecode and codex. sandbox setup writes it, and setup, image build, and doctor default to it (then to Claude Code and Codex). At most 64, unique. |
wrappers | [] | The harnesses whose command runs in a sandbox through the shell wrapper. sandbox enable and disable record it; the wrapper itself lives in your shell rc file. At most 64, unique. |
mcp.import | null | Whether the harness's MCP servers come along. null follows the pack. |
mcp.host_ports | [] | Host ports you agreed to open for MCP servers running on your machine, 1–65535, unique, at most 64. Each port is still checked against the pack and openshell.admin, and DefenseClaw's own listeners are never opened. |
upstream_telemetry | false | Whether OpenShell's own anonymous usage telemetry stays on. sandbox setup turns it off in the OpenShell gateway's environment (OPENSHELL_TELEMETRY_ENABLED=false) unless you keep it (--upstream-telemetry), and records your choice here. On Linux, sandbox doctor reports a mismatch, and --fix repairs it. |
token_delivery | provider | How a sandbox receives its hook ingress credential. provider: as an OpenShell provider credential, so the agent only sees a placeholder, rotated on every start. env: as a plain DEFENSECLAW_SANDBOX_TOKEN variable the agent can read, kept for the sandbox's life. Both are revoked when the sandbox is deleted. See Hook ingress. |
middleware.enabled | false | Reserved for an experimental supervisor-middleware switch. Nothing reads it yet. |
Host patterns in egress.block, egress.allow, egress.unblocked,
admin.egress_block, and admin.egress_allow_only (and in a pack's egress
lists) use the egress proxy's grammar: a host name, *.<host> for every
subdomain (not the host itself), an IP address, or a CIDR prefix such as
198.51.100.0/24. Addresses compare by value, so 2001:db8::1, its
uncompressed spelling, and an IPv4-mapped spelling of an IPv4 address all
match the same entry. A bare * is refused: choose the profile instead
(open allows by default, strict turns the web off). Schemes, ports,
paths, and wildcards inside a name are refused, and so are host names whose
last label does not start with a letter (127.1, 0x7f000001), because
resolvers read them as addresses. Matching ignores case and a trailing dot.
One list widens a host name: on admin.egress_block, example.net blocks
the host and all its subdomains, the way an administrator blocking a domain
means it. Every other list matches a host name exactly.
Lists hold at most 4096 entries.
Project globs (workdir.masks, workdir.unmask, and
sandbox run --unmask, like a pack's masks) are relative to the project folder
and use forward slashes. Absolute paths, ~, drive letters, backslashes, and
.. segments are refused. Lists hold at most 1024 entries.
Names in harnesses, wrappers, image.harness_versions, and
admin.allowed_harnesses start with a letter or digit, then use letters,
digits, ., _, and -, up to 128 characters. An image.harness_versions
key can't contain ., which the configuration loader would read as a nested
key.
Packs at a glance
The built-in packs are:
open, the default: open web through the DefenseClaw egress proxy with the exfiltration blocklist, a live project mount, and skip-permissions mode on. A direct connection request is approved automatically unless the policy refuses it (the blocklists, a public IP address, a port off the list) or it reaches into your machine or private network, which asks.balanced: a curated developer allowlist, and triaged approvals. The allowlist is the egress proxy's allowlist feed, host for host. The proxy can't see inside HTTPS tunnels, so its source hosts (GitHub, GitLab, Bitbucket) accept pushes and uploads as well as fetches; a push still needs a credential inside the sandbox.strict: no web beyond the harness's model provider, manual approvals, copy mode, the harness's own prompts kept, and no MCP servers.
Every built-in pack masks common secret files (.env, .env.*, keys and
certificates, cloud, Kubernetes and Docker credentials) and keeps committed
templates such as .env.example visible (the pack's workspace.unmask).
Whatever the pack, the end-of-session review also flags changes to harness
and agent-tool project config (.claude/, .mcp.json, .codex/,
.cursor/, .gemini/, AGENTS.md, CLAUDE.md, and similar), to lock
files, and to pack.yaml files: a harness run later outside the sandbox acts
on them without asking.
Raising the profile above a pack's own network mode, for example
profile: balanced on the open pack, brings in the balanced pack's
curated allowlist. Allow entries that cover every host or a whole top-level
domain (*.com) are ignored, and custom packs may not contain them.
A custom pack is one strict YAML file with the same sections as the built-in
ones. Unknown or repeated keys, anchors, symbolic links, and built-in names are
refused. The pack file and its directory must belong to you or to root and
must not be writable by their group or by every user. A pack in a sticky
directory that other users can write to, such as /tmp, loads only when root
owns it. Because you own your custom packs, keep them out of folders you mount
into a sandbox: a folder that holds the policy is never shared with the
sandbox. See Write a custom pack.
Admin keys
openshell.admin holds the administrator's limits. Every key is optional; an
unset key adds no limit, and the allow_* switches restrict only when set to
false. Each key is explained, with examples, in
Sandbox policy packs and admin controls.
openshell:
admin: # every key is optional
required_pack: "" # its profile, yolo, workdir mode, MCP import, feeds and ports are floors
required_pack_digest: "" # sha256:<hex> pins the required pack's content
min_profile: "" # strict > balanced > open
allow_yolo: null # null = no constraint; false forbids
allow_mount: null
allow_host_ports: null
allow_unblock: null # also governs approve-always, user allow entries, turning the feed off
allow_learn_mode: null
allowed_harnesses: []
egress_block: [] # always blocked, never unblockable
egress_allow_only: [] # forces an allowlist profile
block_large_uploads: false # true: every sandbox's large uploads to new hosts are cut
require_copy_for: [] # absolute or ~/ path globs that must run in copy mode
max_resources: {cpu: "", memory: ""}
locked: [] # keys run options may not loosen| Key | Values | Effect |
|---|---|---|
required_pack | Pack reference, like pack | Every run uses this pack, and its profile, skip-permissions default, workspace mode, MCP import, blocklist feed, and proxy ports are floors. |
required_pack_digest | sha256: and 64 lowercase hex digits | Pins the required pack's content: the digest defenseclaw sandbox pack show reports. Needs required_pack. |
min_profile | open, balanced, strict | The loosest profile allowed. |
allow_yolo | true, false, null | false keeps every harness's permission prompts. |
allow_mount | true, false, null | false forces copy mode and refuses every host folder mount. |
allow_host_ports | true, false, null | false refuses every host port. |
allow_unblock | true, false, null | false refuses unblocks and approve-always, ignores saved unblocks and the allow entries of users and of custom packs they chose, and keeps the blocklist feed on. A one-time approval then can't reach a feed-listed or private-network destination. |
allow_learn_mode | true, false, null | false refuses learn mode. |
allowed_harnesses | Harness names | Only these harnesses may run. |
egress_block | Host patterns | Always blocked, for every sandbox. Nothing unblocks or approves them. A host name also blocks its subdomains (example.net covers www.example.net). |
egress_allow_only | Host patterns | The only hosts any sandbox may reach. Raises the profile to at least balanced. Entries match exactly: list *.<host> for subdomains. |
block_large_uploads | true, false | true turns the large-upload block on for every sandbox (see egress.block_large_uploads) and keeps its report on: a pack's large_upload_mb: 0 gets 25 MiB, and a threshold above 25 MiB (a pack's or egress.large_upload_mb) is lowered to 25 MiB, or to the required pack's own when that is higher. An unblock still lifts it for one host unless allow_unblock is false. |
require_copy_for | Path globs, at most 1024 | Projects that must run in copy mode. |
max_resources.cpu, max_resources.memory | Same formats as resources | Ceilings for every sandbox. |
locked | Up to 16 of pack, profile, yolo, workdir.mode, workdir.unmask, mcp.import, mcp.host_ports, resources | Keys whose configured value run options may not loosen. |
required_pack pins the pack and makes its posture a floor: your keys and run
options may tighten its profile, skip-permissions default, workspace mode, MCP
import, blocklist feed, and proxy ports, but every attempt to loosen them is
refused. That includes a request to skip permissions or to mount a host folder
when the required pack keeps the prompts or works on a copy. In a
managed_enterprise install, a custom required pack must be a file that root
owns and no other user can modify, together with every directory above it,
such as an absolute path under an administrator-owned directory. The default
pack_dir in the data directory usually does not qualify.
locked keeps the configured value of each listed key against run options
that would loosen it, while options that only tighten it still apply:
--safe, --copy, --no-mcp, a --profile at least as strict as the
configured one, a --pack at least as strict as the configured pack in every
setting (or, when pack itself is not locked, in every setting behind a
locked key, such as the pack's skip-permissions default for yolo), a
smaller --cpu or --memory, and --unmask or --host-port entries the
configuration already lists.
require_copy_for entries are absolute path globs or start with ~/. *,
?, and [...] match within one path segment, and ** across segments. A
relative entry is refused, because it would never match, and a pattern that
starts with **/ covers every mounted folder, since any folder may hold a
match below it. The list also covers mounting a folder above a listed project,
a project folder DefenseClaw can't check, and differently cased spellings of
the path.
Every loosening that openshell.admin refuses is reported as "blocked by
your organization's DefenseClaw policy: <key>", and the same limits gate
runtime actions such as unblocking a destination or opening a host port. In a
managed_enterprise install the administrator owns config.yaml, so the
admin block is authoritative. Elsewhere it is still enforced, but it is
advisory, because you can edit the file. sandbox policy explain and
sandbox doctor say which applies.
Validation and reload
The v8 schema rejects unknown keys in every openshell block. DefenseClaw also
refuses to load a config.yaml whose openshell values break these rules:
ingress_portandegress_portmust differ when both are set;- when
enabledistrue, the effective ingress and egress ports must differ from each other, fromgateway.api_port, and fromguardrail.port. A derived port that would pass 65535 (agateway.api_portof 65534 or higher) must be set explicitly; - enum values, port ranges, host patterns, globs, names, and CPU and memory quantities must be valid, and size and depth values must not be negative;
admin.required_pack_digestmust be a well-formed digest and needsadmin.required_pack, and everyadmin.lockedentry must be a lockable key.
The checks don't open pack files, so a missing or broken custom pack named in
pack or admin.required_pack isn't reported when the config loads.
sandbox policy show reports it, and so does the next sandbox run;
sandbox pack validate checks a pack file on its own.
The gateway reloads the openshell block in place, with either
gateway.config_reload.mode. A change that fails these checks is rejected,
and the running config stays in place. A reload re-resolves the egress policy
of every running sandbox, so changes to the egress keys, the large-upload
threshold included, reach them within seconds.
Turning enabled on or off, or moving ingress_port or
egress_port while enabled is true, restarts the API listener inside the
running gateway. The one exception is entering or leaving the legacy
standalone mode (openshell.mode), which needs a gateway restart.
Legacy keys
mode and sandbox_home remain for the removed legacy standalone sandbox.
They are read only by the legacy bind rule above and by
defenseclaw sandbox legacy-cleanup.
policy_dir, version, auto_pair, and host_networking are accepted and
ignored. No migration rewrites the block.
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 |
block_at | CRITICAL | HIGH | MEDIUM | LOW (any case) — lowest severity that blocks | guardrail.block_at, then the rule pack's level |
alert_at | same values — lowest severity that alerts; never above the block level | guardrail.alert_at, then the rule pack's level |
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.
Enterprise settings (enterprise.*)
A managed deployment (deployment_mode: managed_enterprise) reads an
administrator-owned config: /etc/defenseclaw/config.yaml on Linux,
/opt/cisco/defenseclaw/etc/config.yaml on macOS, and
C:\ProgramData\Cisco\DefenseClaw\etc\config.yaml on Windows. The
enterprise block tunes it. The config is rejected if the block appears in a
config whose deployment_mode is not managed_enterprise. In the
secure_client profile, profile is the only key allowed. For explanations
and examples, see the enterprise settings reference.
| Key | Values (default) | OS | Notes |
|---|---|---|---|
enterprise.profile | secure_client | standalone (Windows and macOS: secure_client; Linux: standalone) | all | Linux accepts only standalone |
enterprise.inspection.ai_defense.enabled | bool (false) | all | Adds Cisco AI Defense to the local engine |
enterprise.inspection.ai_defense.credential | credential name | all | Lowercase letters, digits, and dashes; must be valid when AI Defense is enabled. Store the value with enterprise secret set. An inline cisco_ai_defense.api_key is rejected |
enterprise.enrollment.mode | auto | manifest (auto) | all | With manifest you publish targets.yaml and the enumerator does nothing. On Windows the first install then needs --manifest (Setup: MANIFEST=) |
enterprise.enrollment.include_users | list | all | Linux and macOS: also enrolls these accounts, bypassing the uid range and the login-shell check. Windows: accepted, but every profile is already a candidate, so it changes nothing |
enterprise.enrollment.exclude_users | list | all | Never enrolled; wins over every include. Linux and macOS also match a numeric uid |
enterprise.enrollment.include_groups, exclude_groups | list | all | Linux and macOS: group names or numeric IDs. Windows: local, Active Directory and Microsoft Entra ID groups by name or SID, decided from session tokens (cached for signed-out users) and the local account database |
enterprise.enrollment.exempt_users | list | all | Linux and macOS: inspected but not enrolled. Windows: machine-policy agents keep their rows, so the user is inspected; per-user agents get no new rows |
enterprise.enrollment.unenrolled_users | inspect | deny (inspect) | Linux, macOS | How machine-policy connectors treat users who are not enrolled. On Windows an unenrolled user always fails closed |
enterprise.enrollment.root | inspect | deny | exempt (inspect) | Linux, macOS | root is never enrolled. deny refuses root's hook calls. exempt currently behaves like inspect |
enterprise.enrollment.uid_min | integer (0) | Linux, macOS | 0 reads UID_MIN from /etc/login.defs on Linux (fallback 1000) and uses 501 on macOS |
enterprise.enrollment.uid_max | integer (0) | Linux, macOS | The highest uid enrolled, for local and directory accounts. 0: login.defs UID_MAX (fallback 60000) bounds only local accounts on Linux; macOS uses 2147483646 |
enterprise.enrollment.home_roots | list of absolute paths | Linux, macOS | Extra home parents the guardian may write, beyond /home and /var/home (Linux) or /Users (macOS) |
enterprise.enrollment.agent_prefixes | list of absolute paths | Linux, macOS | Extra administrator install prefixes where the enumerator and guardian look for agent CLIs. Paths under homes and temporary directories are refused |
enterprise.machine_policy.default.* and enterprise.machine_policy.connectors.<name>.* | see the next table | all | Resolution order: the connector's value, then default, then the built-in value. This block does not enable a connector; guardrail.connectors does |
enterprise.trust.mode, enterprise.trust.allowed_signers | authenticode | hash_pinned; SHA-256 thumbprints | Windows | Narrow the standalone lifecycle's payload trust: authenticode refuses a hash-pinned run and any run over a hash-pinned deployment (1639); hash_pinned or unset admits both; allowed_signers pins signers like --allowed-signer. Linux and macOS accept and validate them but do not use them |
enterprise.coexistence.per_user_install | migrate | block | ignore | — | Reserved: accepted and validated, no effect in this release |
enterprise.coexistence.disable_self_update | bool (true) | Windows | Sets HKLM\SOFTWARE\Policies\Cisco\DefenseClaw\DisableSelfUpdate only when the value is absent, which also stops the per-user install.ps1. On every managed host the update notice stays off and the per-user installer, upgrade and rollback refuse, whatever this key says |
enterprise.network.https_proxy, enterprise.network.no_proxy | proxy URL; host list | all | The gateway's outbound proxy: AI Defense, LLM providers (which need an http:// proxy URL), the passthrough, webhooks, the remote model router and telemetry exporters go through it. Linux and macOS also set HTTPS_PROXY and NO_PROXY (both cases) in the gateway's service environment; the Windows gateway sets them in its own process. A change needs a gateway restart |
machine_policy key | Values | Built-in default |
|---|---|---|
ownership | merge | verify_only | off | merge |
managed_hooks_only | enforce | preserve | enforce |
foreign_hooks | remove | report | allow | remove |
higher_precedence_sources | fail | warn | fail |
allowed_hooks | list of SHA-256 digests | none; the default list and the connector's list are merged |
version_floor | enforce | report | off; only under connectors.claudecode | enforce; see Claude Code version floor |
How each OS applies these keys to each connector is on Machine policy.
The standalone lifecycle on Linux and macOS also requires config_version: 8,
enterprise.profile: standalone if you set it, and data_dir set to exactly
/var/lib/defenseclaw (Linux) or /opt/cisco/defenseclaw/runtime (macOS). If
you set gateway.api_bind or gateway.api_port, they must be 127.0.0.1 and
18970. Connectors are enabled by guardrail.connectors, as in any other
config; see Choose the agents to protect.
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 |
| Devin (Devin CLI and Devin Desktop's Devin Local) | ~/.config/devin/config.json or <workspace>/.devin/hooks.v1.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; Google documents no configuration-home environment override, and DefenseClaw owns only this global registration. Antigravity also discovers <workspace>/.agents/hooks.json, which DefenseClaw does not patch to avoid duplicate ownership. |
| 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 when set, then $OMNIGENT_CONFIG_HOME/config.yaml or ~/.omnigent/config.yaml; ~/.defenseclaw/hooks/defenseclaw_omnigent_policy.py; and defenseclaw_omnigent.pth in OmniGent's Python environment. The CLI server must receive the selected YAML through --config. |
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, inspection verdict shape, the /api/v1/sandbox REST API, and the OpenShell sandbox hook ingress and egress proxy. The handler code is authoritative.
Keys
The defenseclaw keys registry, credential resolution order, dynamically discovered credential references, and separately managed gateway tokens.