Reference

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.

defenseclaw-gateway exposes a sidecar management API used by hook scripts, connector plugins, the CLI, and the TUI. This page lists every static route pattern registered by APIServer.Run, the two provider routes installed by registerProviderRoutes, and every built-in connector hook route.

This is not an OpenAPI specification. The canonical request and response shapes are the handlers in internal/gateway/api.go and the files referenced from its mux.HandleFunc registrations.

This page covers the sidecar API on gateway.api_port. The separate guardrail proxy listener on guardrail.port has its own OpenAI-compatible and provider routes in internal/gateway/proxy.go. The routes that manage OpenShell sandboxes are under Sandbox API. The two listeners that sandboxes themselves reach, the hook ingress and the egress proxy, are described under Sandbox listeners.

Bind address

KnobConfig defaultEffective value
gateway.api_port18970Sidecar API port
gateway.api_bindempty127.0.0.1 when empty, except on an uncleaned legacy sandbox host (below)

The API binds to loopback by default. The one exception is a host that still carries the removed legacy standalone sandbox: while the config still says openshell.mode: standalone, an empty gateway.api_bind inherits guardrail.host when that value is non-empty and is not the literal localhost (for example 10.200.0.1), so upgrade health checks keep reaching the gateway. Running defenseclaw sandbox legacy-cleanup resets that config and returns the API to loopback. An explicit gateway.api_bind always takes precedence. A non-loopback value expands access to the authenticated management surface; protect that listener with host firewalling and a deployment-specific network design. gateway.host and gateway.port describe the upstream agent gateway, while guardrail.host/guardrail.port control the separate guardrail proxy listener.

Auth

Every request except GET /health must authenticate. The shared authentication middleware accepts these header forms; all three are constant-time compared with the resolved gateway token:

Authorization:         Bearer <token>
X-DefenseClaw-Token:   <token>
X-DC-Auth:             Bearer <token>

PATCH /v1/guardrail/config performs a second, endpoint-local check and accepts only Authorization: Bearer or X-DefenseClaw-Token; X-DC-Auth alone is rejected there. The endpoint also rejects every patch in managed_enterprise deployment mode.

Loopback connector hook routes can instead use the matching connector's narrow hook token. The Codex token also authorizes /api/v1/codex/notify. A connector-scoped token on a generic /api/v1/inspect/* route is accepted only over loopback and only when X-DefenseClaw-Connector names the same registered connector.

OTLP credentials are source-specific for Codex and Claude Code. They send their scoped bearer to /v1/{logs,metrics,traces} together with X-DefenseClaw-Source: codex|claudecode; that header selects and must match the scoped credential. Once a scoped token exists for one of those sources, the master gateway token is rejected for that source's scoped route. Copilot CLI upstream documents optional OTel traces and metrics, but DefenseClaw does not configure, authenticate, ingest as a Copilot-native source, or certify that surface. Copilot connector telemetry therefore reaches this API through its authenticated hook route only. OmniGent's separately documented optional native source currently authenticates loopback /v1/{signal} requests with the master gateway token.

The token is resolved from one of, in priority order:

  1. The env var named by gateway.token_env (custom name — operator intent wins)
  2. DEFENSECLAW_GATEWAY_TOKEN (canonical)
  3. OPENCLAW_GATEWAY_TOKEN (legacy fallback for existing installs)
  4. gateway.token directly in ~/.defenseclaw/config.yaml

There is no standalone ~/.defenseclaw/gateway-token file. On first boot the sidecar creates DEFENSECLAW_GATEWAY_TOKEN in ~/.defenseclaw/.env when neither a supported environment nor dotenv source already supplies a token. Rotate a locally managed token with defenseclaw setup rotate-token --yes; that transaction restarts the gateway and refreshes active connector credentials. An externally managed custom gateway.token_env must be rotated by its owning secret system.

A request that carries an OpenShell sandbox binding credential (a dcsb_ value in Authorization, X-DefenseClaw-Token, X-DC-Auth, or an OTLP path token) gets 401 on every authenticated route of this API, before any other check. Those credentials are valid only on the hook ingress.

CSRF protection

Every non-GET/HEAD request also requires X-DefenseClaw-Client (any non-empty caller identifier such as cli, openclaw-plugin, or inspect-hook/1.0). POST, PUT, PATCH, and DELETE requests require Content-Type: application/json; OTLP requests instead require application/json or application/x-protobuf. OPTIONS requires the client header but no content type. The only narrow client-header exception is a loopback scoped-token OTLP path, because some native exporters cannot add arbitrary headers; that path still requires an OTLP content type and rejects a non-local browser origin.

All ordinary mutations also reject a non-local Origin; cross-site requests are rejected when Sec-Fetch-Site identifies them. A shared middleware caps ordinary POST, PUT, and PATCH bodies at 1 MiB. Exact authenticated OTLP-HTTP ingest routes use a separate 64 MiB receiver limit so exporter batches can follow the protocol's recommended bound without increasing the limit for the rest of the API.

X-DefenseClaw-Client: cli

Endpoints

Health & status

MethodPathAuthPurpose
GET/healthexemptLiveness + uptime + version. Used by k8s probes and defenseclaw doctor.
GET/statusrequiredHealth, process identity, provenance, an optional connected-gateway hello, legacy singular connector_mode, and connector_modes (one entry per active connector). Used by defenseclaw status and the TUI.
GET/v1/connectorsrequiredLists every connector registered in the gateway (name, description, source, hook capabilities, tool inspection mode).

When openshell.enabled is true, /health includes a sandbox subsystem for the OpenShell sandbox runtime:

  • running, with the hook ingress and egress proxy addresses in details.ingress and details.egress;
  • degraded, with a last_error that names each failing part: ingress, egress, manager, or openshell (the connection to the OpenShell gateway). Sandbox hooks fail closed while a part is down;
  • disabled, with the reason, on a platform or in a deployment that can't run sandboxes: any platform but Linux and macOS, or a managed_enterprise install;
  • error, with the cause in last_error, when the sandbox runtime can't be set up at all, for example because its binding store or image store can't be opened. The sandbox routes then answer 503 unavailable, and sandbox hooks fail closed;
  • stopped while the API listener restarts.

On a host that still carries the removed legacy standalone sandbox (see Bind address), /health includes a sandbox subsystem with state degraded and a last_error of "legacy standalone install detected", directing the operator to run defenseclaw sandbox legacy-cleanup. On a host with neither, where openshell.enabled is false and no legacy install is left, the sandbox subsystem is absent from /health.

GET /status carries both a singular and a plural connector field so old and new clients keep working on a multi-connector install:

GET /status (connector fields example)
{
  "connector_mode": {
    "connector": "codex",
    "mode": "observability",
    "policy_mode": "action",
    "guardrail_mode": "action",
    "hook_fail_mode": "closed",
    "enabled": true,
    "hook_enforcement": true,
    "enforcement_surface": "agent_lifecycle_hooks",
    "telemetry": ["hooks", "otel", "notify"],
    "proxy_intercept": false
  },
  "connector_modes": [
    {
      "connector": "codex",
      "mode": "observability",
      "policy_mode": "action",
      "guardrail_mode": "action",
      "hook_fail_mode": "closed",
      "enabled": true,
      "hook_enforcement": true,
      "enforcement_surface": "agent_lifecycle_hooks",
      "telemetry": ["hooks", "otel", "notify"],
      "proxy_intercept": false
    },
    {
      "connector": "claudecode",
      "mode": "observability",
      "policy_mode": "observe",
      "guardrail_mode": "observe",
      "hook_fail_mode": "closed",
      "enabled": true,
      "hook_enforcement": false,
      "enforcement_surface": "agent_lifecycle_hooks",
      "telemetry": ["hooks", "otel"],
      "proxy_intercept": false
    }
  ]
}
  • connector_mode (singular) is the same summary object for the primary connector, retained for clients written before multi-connector existed. On a fan-out install it is not the authoritative roster view; use connector_modes instead.
  • connector_modes (plural) is the authoritative per-connector view: one entry per active connector. mode is the backward-compatible data-path value (guardrail or observability); policy_mode and guardrail_mode report the effective observe/action posture; hook_fail_mode is the effective hook fail mode; enabled is the connector's effective guardrail enablement; hook_enforcement is true for a non-proxy connector in action mode; enforcement_surface identifies the proxy, lifecycle-hook, or OmniGent policy API path; telemetry lists reported wiring channels; and proxy_intercept is true only for proxy connectors. mode, enforcement_surface, and proxy_intercept are derived from the connector's declared LLM traffic mode, through the same predicate the sidecar uses to decide whether to bind the proxy listener, so this array cannot disagree with what is actually in the data path. defenseclaw-gateway status renders its per-connector "Connector Mode" section directly from this array.

Connector hooks

Built-in connectors that implement the hook endpoint contract register the following routes. Proxy connectors such as OpenClaw and ZeptoClaw do not register a connector hook route.

MethodPathConnectorSource
POST/api/v1/claude-code/hookClaude Codeinternal/gateway/connector/claudecode.go
POST/api/v1/codex/hookCodexinternal/gateway/connector/codex.go
POST/api/v1/cursor/hookCursorinternal/gateway/connector/hook_only.go
POST/api/v1/devin/hookDevinsame; native CLI lifecycle events only
POST/api/v1/copilot/hookGitHub Copilot CLIsame
POST/api/v1/openhands/hookOpenHandssame
POST/api/v1/antigravity/hookAntigravitysame
POST/api/v1/hermes/hookHermessame
POST/api/v1/opencode/hookOpenCodesame
POST/api/v1/amp/hookAmpinternal/gateway/connector/amp.go
POST/api/v1/omnigent/hookOmniGentinternal/gateway/connector/omnigent.go

All hook endpoints share a per-IP rate limiter (20 RPS, burst 40 — see hookLimiter in api.go). Loopback callers (the on-host hook scripts) are exempt; the limit only applies to remote callers.

Inspection (rate-limited)

The /api/v1/inspect/* family is the hot path that connector hooks and the OpenClaw plugin call to score a single tool call, prompt, or completion. Mounted under a sub-mux that wraps the per-IP rate limiter.

MethodPathUsed byPurpose
POST/api/v1/inspect/toolShared hook script; OpenClaw extensionInspect a tool call (tool + args), or inspect message content when tool: "message" supplies content and direction. Returns the verdict envelope below.
POST/api/v1/inspect/requestShared hook scriptInspect an outbound LLM request (prompt).
POST/api/v1/inspect/responseShared hook scriptInspect a model response.
POST/api/v1/inspect/tool-responseShared hook scriptInspect a tool's output before it returns to the agent.

/api/v1/inspect/tool is the existing trusted tool-call entry point; there is no client-supplied is_tool_call switch and no additional semantic endpoint. The authenticated route and its server-side connector identity establish the boundary. A request with tool: "message" stays in the message lane and is not eligible for structured tool-call CEL rules.

Single-call semantic findings are available on this inspect route and on native pre-tool hooks. Ordered tool chains are stricter: they use durable SQLite state only on authenticated /api/v1/{connector}/hook events with canonical connector and session correlation. Inspect, proxy, router, and OTLP traffic never advance a chain. Only a matching authoritative successful result can promote an existing pending proposal into a durable successful predecessor. A chain can deny only its final step on an enforcement-safe synchronous pre-tool event. Post-tool and tool-result events never make the final blocking decision. See the stateful connector lifecycle for the pairing, state-level, and cutoff contracts. Chain matches are HIGH severity and retain the connector's effective action matrix: default and permissive profiles alert, strict blocks, and eligible human-in-the-loop configurations confirm. A durable deny receipt is created only when that effective action is block.

Ordered chainWall-clock windowEvent horizon
Guardrails disabled → external egress30 minutes64 events
Permission denied → runtime bypass5 minutes16 events
Privilege discovery → elevation15 minutes32 events
Secret-manager read → external egress30 minutes64 events
Sensitive secret read → external egress30 minutes64 events
Workload identity access → lateral execution15 minutes32 events

Chain state contains bounded masks and fingerprints rather than raw commands, arguments, paths, endpoints, or tool payloads. Durable deny receipts replay the same final decision without duplicating finding telemetry.

Code & network scanning

MethodPathPurpose
POST/api/v1/scan/codeRun the built-in CodeGuard scanner on the local filesystem path supplied in the JSON body.
GET/api/v1/network-egressList persisted egress audit records. Supports limit, hostname, session_id, agent_id, root_agent_id, user_id, blocked, and RFC3339 since filters.
POST/api/v1/network-egressIngest one observed egress event into the audit logger; this is not an allow/block decision endpoint.

Asset scanning

MethodPathPurpose
POST/v1/skill/scanRun the skill scanner against the local target directory named in the request.
POST/v1/plugin/scanRun the plugin scanner against the local target directory named in the request.
POST/v1/mcp/scanRun the MCP scanner against a target URL or local path.
POST/v1/skill/fetchStream a tar.gz of an existing local directory after resolving it beneath a configured skill or plugin root; this does not fetch from a registry.
POST/scan/resultPersist a scanner result through the canonical audit logger.

Asset enable / disable

MethodPathPurpose
POST/skill/disableAsk the connected upstream agent gateway to disable a skill by key (skills.update).
POST/skill/enableAsk the connected upstream agent gateway to enable a skill by key (skills.update).
POST/plugin/disableAsk the connected upstream/OpenClaw gateway to disable a plugin through its config RPC.
POST/plugin/enableAsk the connected upstream/OpenClaw gateway to enable a plugin through its config RPC.

These routes return 503 when no upstream client object is present and 502 when an attempted upstream RPC fails, including a disconnected client. They do not perform the CLI's filesystem quarantine/restore workflow.

Inventory

MethodPathPurpose
GET/skillsReturn the connected upstream agent gateway's skills.status RPC payload. An absent client returns 503; an RPC/disconnection failure returns 502.
GET/mcpsReturn MCP server entries for the singular active connector from DefenseClaw configuration; unavailable or malformed input returns an empty list.
GET/tools/catalogReturn the one connected upstream agent gateway's tools.catalog RPC payload. An absent client returns 503; an RPC/disconnection failure returns 502.

Provider registry

MethodPathPurpose
GET, HEAD/v1/config/providersReturn the merged built-in and operator provider registry plus overlay status.
POST/v1/config/providers/reloadReload the built-in registry and custom-providers.json overlay.

Policy

MethodPathPurpose
POST/policy/evaluateDebug the admission policy with {domain?: "admission", input: {target_type, target_name, path?, scan_result?}}. DefenseClaw injects its current allow/block lists and returns {ok: true, data: <AdmissionOutput>}; arbitrary OPA domains/input are rejected.
POST/policy/evaluate/firewallEvaluate the firewall sub-policy in isolation.
POST/policy/evaluate/auditEvaluate the audit sub-policy.
POST/policy/evaluate/skill-actionsEvaluate the skill_actions sub-policy by severity.
POST/policy/reloadAtomically reload .rego modules plus optional data.json from configured policy_dir (or its rego child), invalidate the judge cache, and return {"status":"reloaded","policy_dir":"..."}.

Guardrail control plane

MethodPathPurpose
POST/v1/guardrail/eventEmit canonical telemetry for an already-decided guardrail event. Requires evaluation_id, direction (prompt or completion), action (allow, alert, or block), and severity (NONE through CRITICAL); latency/token values must be finite and nonnegative. Returns {"status":"ok"}. No production caller is wired in this repository.
POST/v1/guardrail/evaluateCombine caller-supplied, precomputed local_result / cisco_result values with required evaluation_id, direction, and mode, plus optional model/scanner/content/timing metadata. It does not scan arbitrary content and returns a GuardrailOutput with action, severity, reason, and scanner_sources.
GET/v1/guardrail/configReturn the active guardrail config snapshot.
PATCH/v1/guardrail/configPatch supported guardrail runtime fields into the active DefenseClaw config file, validate the full YAML file, and apply it through the central reloader. Body is JSON, not raw YAML. Supported fields: mode, scanner_mode, block_message, connector, hilt_enabled, hilt_min_severity. Managed-enterprise deployments reject this API mutation.

Changing connector uses restart semantics so listener and hook state are rebuilt; it is not accepted as an in-place connector swap.

Enforcement logging

These endpoints manage the durable enforcement allow/block lists.

MethodPathPurpose
POST, DELETE/enforce/blockAdd a block, or remove it with DELETE.
POST/enforce/allowAdd an allow and re-enable a disabled skill/plugin when required.
GET/enforce/blockedList blocked entries.
GET/enforce/allowedList allowed entries.

Audit

MethodPathPurpose
POST/audit/eventAppend one audit-event JSON object to the configured audit logger/store. The production OpenClaw extension client uses this route; managed connector hook routes audit internally.
GET/alertsReturn recent alert rows. limit defaults to 50 and is capped at 500.
POST/api/v1/alerts/dispositionPreview or apply an audited alert acknowledgement/disposition operation.

There is no general audit-history or streaming HTTP route. Read/export history through the CLI:

defenseclaw-gateway audit export --output audit.jsonl     # JSONL of audit_events
tail -f ~/.defenseclaw/gateway.jsonl | jq                  # Optional configured v8 JSONL destination
defenseclaw alerts                                          # Recent alerts (paginated)
defenseclaw tui                                             # Live dashboard

Audit export includes the legacy details text and, when present, first-class structured JSON from audit_events.structured_json. Connector hook rows use schema: "defenseclaw.hook.v1"; older parsers can still read the mirrored details_json= token inside details.

Config

MethodPathPurpose
POST/config/patchBridge {path, value} to the connected upstream agent gateway's WebSocket config.patch RPC. It does not write DefenseClaw's config.yaml; an absent client returns 503, while an RPC/disconnection failure returns 502.

Runtime control and internal observability bridges

These authenticated routes are narrow process/CLI integration seams rather than general-purpose ingestion APIs.

MethodPathPurpose
POST/api/v1/admin/shutdownRequest graceful shutdown. Restricted to loopback and requires the current PID and data directory identity.
POST/api/v1/telemetry/canaryEmit a trace canary through the active observability-v8 runtime.
POST/api/v1/watchdog/recoveryRecord a watchdog recovery metric. Restricted to loopback.
POST/api/v1/observability/destination-test/activityPersist destination-test compliance activity. Restricted to loopback and the exact python-cli client marker.
POST/api/v1/observability/cliHand validated CLI events to the process-owned observability-v8 runtime.

AI usage / discovery (AIBOM)

The continuous AI Discovery surface — see AI Discovery for the operator workflow.

MethodPathPurpose
POST/api/v1/agents/discoveryReceive an agent-discovery report from the on-host scanner.
GET/api/v1/ai-usageCurrent continuous-discovery snapshot (summary plus agent, process, endpoint, component, and local-model signals). Local responses retain each model's dedicated model block.
POST/api/v1/ai-usage/scanTrigger an on-demand AI usage scan.
POST/api/v1/ai-usage/discoveryIngest a validated external AI-discovery report.
GET/api/v1/ai-usage/componentsAggregated components across the active workspace.
GET/api/v1/ai-usage/components/{ecosystem}/{name}/locationsWhere the component was detected.
GET/api/v1/ai-usage/components/{ecosystem}/{name}/historyDetection history for one component.
GET/api/v1/ai-usage/confidence/policyShow the active confidence-scoring policy.
POST/api/v1/ai-usage/confidence/policy/validateDry-run validation of a candidate policy file.

Correlation ledger

Each read route accepts exactly one supported identity anchor (for example record_id, evaluation_id is not a supported query key here, semantic_event_id, session_id, trace_id, or tool_invocation_id) and supports bounded cursor pagination.

MethodPathPurpose
GET/api/v1/correlation/graphReturn the evidence-backed identity graph around an anchor.
GET/api/v1/correlation/explainExplain how records are linked to an anchor.
GET/api/v1/correlation/timelineReturn the correlated event timeline.
GET/api/v1/correlation/conflictsReturn identity conflicts associated with an anchor.

Codex bridge

MethodPathPurpose
POST/api/v1/codex/notifyCodex agent-turn-complete notifier. The Codex notify-bridge.sh shim posts each turn's JSON arg here so the gateway can audit turn counts and completion reasons.

OTLP receiver

The gateway accepts OTLP-HTTP from configured connectors. Both OTLP JSON and protobuf content types are accepted; decoding is implemented in internal/gateway/otel_ingest.go. Request bodies are bounded at 64 MiB; larger batches receive HTTP 413 and are recorded as content-free body_too_large rejections.

MethodPathPurpose
POST/v1/logsIngest OTLP logs.
POST/v1/metricsIngest OTLP metrics.
POST/v1/tracesIngest OTLP traces.
POST/otlp/{source}/{token}/v1/{signal}Connector-scoped logs, metrics, or traces for a native exporter that cannot set an auth header. The path token is accepted only over loopback and is removed from route telemetry.

See internal/gateway/otel_ingest.go for the parsing details and Local observability for the operator setup.

Sandbox management

MethodPathPurpose
GET, POST, DELETE/api/v1/sandbox/...Create, inspect, and control OpenShell sandboxes, answer their asks, lift egress blocks, explain their policy, and stream their activity. See Sandbox API.

Verdict envelope

All four /api/v1/inspect/* endpoints serialize the same ToolInspectVerdict:

{
  "action":     "block | confirm | alert | allow",
  "raw_action": "block",
  "severity":   "CRITICAL | HIGH | MEDIUM | LOW | INFO | NONE",
  "confidence": 0.93,
  "reason":     "DefenseClaw policy blocked this action (rule CMD-RM-RF: Recursive force delete from critical root path). Do not retry it in another form.",
  "findings":   ["CMD-RM-RF:Recursive force delete from critical root path"],
  "detailed_findings": [
    {
      "rule_id":    "CMD-RM-RF",
      "title":      "Recursive force delete from critical root path",
      "severity":   "CRITICAL",
      "confidence": 0.95,
      "evidence":   "rm -rf /",
      "tags":       ["destructive", "shell"]
    }
  ],
  "mode":       "action | observe",
  "would_block": true,
  "approval_timeout_ms": 45000
}

Notable behaviours:

  • reason is the text the agent shows. For a block or confirmation by a policy rule it says that DefenseClaw policy decided and names the rule by ID; a title is kept only for DefenseClaw's built-in rules, and rule-pack titles and scanner text stay in the audit record, which keeps the full source reason. A block also tells the agent not to retry the action in another form. In the standalone enterprise profile the text names the organization's policy and tells the user to contact the administrator.
  • action is the effective verdict, not the raw policy decision. applyMode() downgrades block, confirm, and alert to allow outside action mode and preserves the original in raw_action. It sets would_block: true only for a downgraded block; downgraded confirm and alert leave that field false/omitted.
  • confirm is surface-dependent. A native OpenClaw tool-approval caller can receive confirm and resolve it in the plugin. A non-native caller cannot safely pause, so the inspect handler fails the confirmation closed to block. Proxy-lane confirmations may be resolved by the in-process HILTApprovalManager; unsupported proxy surfaces degrade to alert. There is no /v1/hilt/* HTTP API.
  • evaluation_id is the runtime join key in telemetry. Inspect calls stamp it on structured JSONL and audit/scan rows, but the current ToolInspectVerdict HTTP schema does not expose evaluation_id or rule_ids. Connector-hook responses do expose both fields, as shown below. See Observability §1.4 for the per-surface contract.
  • Pattern-rule and CodeGuard decisions populate both representations for backward compatibility. Clean verdicts can omit details, and static, MCP, or AID merge paths can add string findings without synthesizing matching detailed_findings.
  • Optional fields are omitted when empty. In particular, approval_timeout_ms is absent when its value is zero. A structured finding can additionally include tool_capability_class; it has no remediation field.
  • Empty request/response content takes a fast allow path. Those two handlers return the same verdict type, but before applyMode() runs; the serialized non-optional mode is therefore an empty string and raw_action is absent.

Other endpoints return their own shapes — /v1/skill/scan returns a scanner-specific envelope, /audit/event returns {"status": "ok"}, and /skill/disable returns {"status": "disabled", "skillKey": "..."}. Always confirm against the handler code before assuming a shape.

Connector hook responses

A connector hook response can carry evaluation_id and the top rule_ids when the evaluation produced correlation data:

{
  "action":        "allow | block | deny | …",
  "reason":        "rule X fired",
  "findings":      ["rule.id.one:First finding", "rule.id.two:Second finding"],
  "evaluation_id": "eval-c1d0…",
  "rule_ids":      ["rule.id.one", "rule.id.two"]
}

Connector-hook responses expose a string findings list, not the inspect endpoint's detailed_findings objects. Pattern-rule entries normally use rule_id:title; some scanner-specific paths emit bare finding IDs. rule_ids is the canonical bare-ID list populated with evaluation_id when the hook evaluation produced correlation data.

For /api/v1/inspect/*, evidence is redacted by default. X-DefenseClaw-Reveal-PII: 1 requests raw response evidence. The tool-inspect handler records an inspect-reveal audit event; the request, response, and tool-response handlers currently reveal without that extra audit event.

Sandbox API

The routes under /api/v1/sandbox/ manage NVIDIA OpenShell sandboxes. The defenseclaw sandbox CLI, the TUI Sandboxes panel, and the macOS app call them. The daemon is the only writer of sandbox bindings, approvals, and the sandboxes' OpenShell policy rules, and it creates and changes the OpenShell sandboxes and providers. Two CLI commands also write to the OpenShell gateway directly: sandbox teardown deletes the sandboxes and providers the daemon doesn't delete (it is down, or doesn't know them) and removes unused DefenseClaw provider profiles, and sandbox setup imports the hook ingress provider profile when the gateway lacks it. The CLI keeps the terminal, the copy-mode workspace, the image builds, and the installer; see Sandbox CLI. The wire types are in internal/openshell/sandboxapi, and the router is internal/gateway/api_sandbox.go.

Auth and CSRF. The routes sit on the main API port behind the same middleware as every other route. They need the master gateway token (see Auth); a sandbox binding credential gets 401. Mutating requests carry X-DefenseClaw-Client (the CLI sends defenseclaw-sandbox) and Content-Type: application/json (see CSRF protection). Bodies are decoded strictly: an unknown field, trailing data, or malformed JSON gets 400 invalid_request, and a body over 256 KiB gets 413. Where the table marks a body optional, an empty body means the defaults.

When sandboxes are off. Until the sandbox runtime runs, every route except GET /api/v1/sandbox/status answers 503: code disabled when openshell.enabled is false, and unavailable when it is true but the runtime isn't running (see the /health sandbox subsystem above). GET /api/v1/sandbox/status then still answers 200, with enabled and the reason.

MethodPathRequestResponse
GET/api/v1/sandbox/status—Status: enabled, available and reason, the OpenShell gateway (with the compute driver it runs: docker, or vm for OpenShell's MicroVM driver), ingress_addr and egress_addr, the configured pack and profile, admin (see below), counts of sandboxes, running, and pending_approvals, last_reconcile, started_at (when the sandbox runtime started: nothing keeps the sandboxes' hook and egress counters across a restart, so they count from then), and daemon_uid (the uid the daemon runs as; absent on Windows).
GET/api/v1/sandbox/sandboxes—{"sandboxes": [Sandbox]}
POST/api/v1/sandbox/sandboxesCreateRequestSandbox
GET/api/v1/sandbox/sandboxes/{name}—Sandbox
DELETE/api/v1/sandbox/sandboxes/{name}Optional {"keep_snapshot": bool}DeleteResponse: name, deleted, the removed providers, warnings
POST/api/v1/sandbox/sandboxes/{name}/stopOptional {}Sandbox
POST/api/v1/sandbox/sandboxes/{name}/startOptional {"no_snapshot": bool, "new_snapshot": bool}; 409 conflict when the sandbox is already runningSandbox
POST/api/v1/sandbox/sandboxes/{name}/undoOptional {"preview", "keep_refs", "stop", "restart"} (all bool)UndoResponse: the workspace result, whether it stopped and restarted the sandbox, a summary, and a review
POST/api/v1/sandbox/sandboxes/{name}/reviewOptional {"diff": bool}ReviewResponse: the report, a summary, a risk_line, and the diff when asked
POST/api/v1/sandbox/sandboxes/{name}/acceptOptional {"snapshot_created_at": time, "session": int}; 409 conflict when the sandbox is running, its undo point was undone, it is another one than snapshot_created_at names, or the sandbox was started again since the session named; 400 invalid_request for a copy-mode sandbox; 404 not_found without an undo pointSandbox, whose snapshot carries accepted_at
GET/api/v1/sandbox/sandboxes/{name}/logsQuery lines (optional, 0 or absent for all of it); 404 not_found when no log was keptRunLog: name, state (exited or interrupted), exit, started_at, kept_at, log
POST/api/v1/sandbox/sandboxes/{name}/workspaceWorkspaceReport{"status": "recorded"}
GET/api/v1/sandbox/approvalsQuery sandbox (optional){"approvals": [Approval]}
POST/api/v1/sandbox/approvals/{id}ApprovalDecisionApprovalResult: the approval, whether it was persisted, a message
POST/api/v1/sandbox/egress/unblockUnblockRequestUnblockResponse: host, sandbox, scope (sandbox or always), persisted, message
GET/api/v1/sandbox/policy/explainQuery, see belowExplain
GET/api/v1/sandbox/activityQuery sandbox, since, follow{"events": [ActivityEvent]}, or a server-sent event stream

Any other path under /api/v1/sandbox/ gets 404 not_found.

Sandbox requests and responses

POST /api/v1/sandbox/sandboxes creates and starts a sandbox. The CLI sends it for sandbox run.

FieldMeaning
harnessRequired. The connector to run, such as claudecode or codex.
projectRequired. The absolute host path of the launch folder. In mount mode the daemon checks and mounts it. In copy mode it only labels the sandbox: the CLI stages and uploads the copy after the create, then reports it with POST …/{name}/workspace.
nameOptional. At most 19 lowercase letters, digits, and - (OpenShell 0.1.1 refuses longer names). Empty picks <folder>-<random>, the folder name cut to fit.
pack, profile, copy, safe, yolo, context, unmask, host_ports, no_mcp, learn, cpu, memoryThe run options that take part in policy resolution.
no_snapshotSkip the pre-session snapshot of a mounted project, and so undo.
no_buildRefuse, with 409 image_unavailable, instead of building and hook-checking a missing harness image inside the request. A build can take minutes.
llmThe model credential: profile (a provider profile the harness supports, such as defenseclaw-anthropic), credentials (its environment variable names and secret values), and bedrock_region.
credentials--credential bindings: name, value, host, and port (default 443).
envNon-secret environment variables. DefenseClaw, proxy, and loader variables, and variables the harness image pins, are refused.

Secret values in llm.credentials and credentials go to OpenShell as provider credentials. The daemon never stores them or returns them.

A Sandbox carries its name and OpenShell id, the harness, its phase (lowercase OpenShell phase such as ready or stopped, or missing when OpenShell no longer has it), and its policy: pack, pack_digest, profile, network_mode, approvals, and yolo. It also carries workdir_mode (mount or copy), project, and workdir, the image (image, image_id, and on a MicroVM gateway the run_image and run_image_id it boots, which carries its run's harness settings) and tamper_tier, hooks (hook traffic, blocked tool calls, the last block's plain reason, tamper counts, the verdicts per hook event under the harness's own event names in events, at most 48 names with the rest in other_events, and whether hooks are silent or unreachable), endpoints (the last result of each credentialed endpoint), egress totals, pending_approvals, the workspace and mcp banner views, snapshot, session (a count that goes up each time the daemon sees the sandbox become ready), violations (settings the policy clamped), warnings, orphaned, and nested_repos (repositories the nested-repository guard found in a mounted project). launch holds what the CLI needs to start the harness. credentials lists the sandbox's --credential bindings (name, host and port, never the value) and host_ports the host ports it may ask to reach: what a resume keeps.

Undo point and detached runs

A mounted sandbox's snapshot is its undo point: kind, ref, created_at, undone_at once undo ran, and accepted_at once the user kept the changes made on top of it. Every start takes a fresh snapshot unless the folder still holds changes an earlier session made that nobody undid or accepted, which keeps the undo point so undo still reverts them, whoever starts the sandbox. POST …/{name}/accept records the acceptance of a stopped sandbox's changes (the CLI sends it when the user keeps them at the end of a session, with the created_at of the snapshot it reviewed them against and the sandbox's session then, so an accept after another start, whose changes nobody reviewed, is refused). The next start then takes a fresh snapshot and drops the acceptance; a start with no_snapshot keeps the old snapshot but drops the acceptance too. A start that fails while OpenShell still reports the sandbox stopped keeps the acceptance for the next one. new_snapshot on a start accepts the changes too.

Every stop of a running sandbox (…/stop, …/undo with stop, and a tamper stop under hooks.on_tamper: stop) looks at the sandbox's latest detached run (sandbox run --detach) before it ends the harness. A run still going is marked interrupted, and the feed gets a sandbox.lifecycle event with reason run_interrupted. Once the harness has exited and the sandbox reads stopping, the daemon keeps the last 1 MiB of the run's log on the host (under <data_dir>/sandboxes/<name>/runlog/), which GET …/{name}/logs serves while the sandbox is stopped, and which sandbox logs of a stopped sandbox prints. A tamper stop keeps no log: it waits on nothing the workload controls, and its run_interrupted event says the log was not kept. The next stop that finds a run replaces it; a delete removes it. Bytes of the log that are not UTF-8 come back replaced.

Approvals and unblocks

An Approval is one of the rare asks: an OpenShell draft proposal that triage would not decide on its own. It has an id, sandbox, kind (network_rule, or host_port for a port on this machine), host, port, endpoints, the binary that connected, whether it is risky, the triage reason, and the OpenShell advisor's rationale and security_notes. Its status is pending, queued, approved, rejected, or failed.

POST /api/v1/sandbox/approvals/{id} takes {"decision": "approve"|"reject", "always": bool, "reason": text}. An approval is usually answered queued: approvals are applied in batches when the sandbox's hooks are quiet, because every OpenShell policy change closes the sandbox's open connections. always keeps the decision for future sandboxes: an approval adds the hosts to openshell.egress.unblocked, a rejection to openshell.egress.block.

POST /api/v1/sandbox/egress/unblock takes {"host", "sandbox", "always"}. sandbox scopes the unblock to one sandbox. Without it, always is required, and the host is saved to openshell.egress.unblocked for every sandbox. Only refusals the policy marks unblockable can be lifted; see How a destination is decided.

Policy explain

GET /api/v1/sandbox/policy/explain resolves the sandbox policy with its provenance. The query names either sandbox (the policy that sandbox runs with, against the current config) or the run options harness, pack, profile, project, copy, safe, yolo, and unmask (repeatable). The Explain response has the pack, pack_source, and pack_digest, the profile, network_mode, and approvals, the admin status, settings, and violations. Before a create on a MicroVM gateway, vm_first_boot says the image the sandbox would boot has no prepared MicroVM disk yet, so its first start takes about a minute.

For a harness whose run files depend on the create request (Claude Code, Codex), run=true describes that request with the following parameters:

  • run_env=KEY=VALUE (repeatable): the env variables the run files read.
  • run_env_withheld=KEY (repeatable): such a variable whose value is not sent because it may be a secret. The answer then counts as a first boot.
  • run_credential=NAME (repeatable): the credential names, never their values.
  • run_llm_profile and run_bedrock_region: the model profile and region.

Without run, the daemon uses the newest sandbox of the harness.

Each setting has a key, value, source (pack, user, flag, admin, gateway, or default), origin (such as openshell.admin.min_profile), and, for a clamped setting, the value requested. gateway is the compute driver of the OpenShell gateway (origin openshell.gateway.compute_driver): OpenShell's MicroVM driver mounts no host folders, so it runs every project on a copy. admin has configured, authority (authoritative in managed_enterprise, otherwise advisory), and detail.

Activity stream

GET /api/v1/sandbox/activity returns the buffered activity feed as {"events": [...]}. With follow=true, or Accept: text/event-stream, it streams server-sent events instead. The stream first replays the buffered events after since, or after the Last-Event-ID header, which wins over since. Then it sends each new event as:

id: 42
event: activity
data: {"seq":42,"time":"2026-09-27T10:15:03Z","kind":"egress.blocked","sandbox":"myapp-7f3a","host":"webhook.site","port":443,"source":"dc-egress-proxy","category":"webhook_catcher","unblockable":true}

A : keepalive comment follows the replay and repeats every 15 seconds. sandbox limits the feed to one sandbox. Too many open streams get 429.

Each ActivityEvent has a seq (one higher per event), time, kind, and sandbox, plus the fields of its kind: host, port, method, source (dc-egress-proxy or openshell), category, rule, unblockable, bytes_up, bytes_down, threshold, approval_id, tool, event, phase, severity, reason, and message.

kindMeaning
egress.allowed, egress.blockedA destination the egress proxy or OpenShell allowed or blocked. An upload the large-upload block cut, and later refused uploads to that host, are egress.blocked with category: "large_upload".
egress.large_uploadA large upload to a destination the sandbox hadn't contacted before, which only the report saw (the block was off, or the host is exempt). It is reported as it crosses the sandbox's threshold, before it ends: bytes_up is what had gone up then, and message says more than the threshold (⚠ large upload to first-seen files.example.net (more than 25 MiB)).
egress.unblockedAn unblock was applied.
approval.requested, approval.resolvedAn ask was raised or answered.
tool.blockedDefenseClaw blocked a tool call. reason is plain and never quotes matched content.
sandbox.lifecycleA sandbox changed phase.
findingA finding. Its reason is, for example, hook_tamper (a tool ran without a DefenseClaw verdict), hook_silence, hooks_unreachable (hooks do not reach DefenseClaw, so every tool call is blocked), hooks_restored, or nested_repo.
workspaceAn undo, or a copy-mode upload or pull.
droppedA slow subscriber missed events.

Sandbox API errors

Every error answer is JSON: {"code": "…", "error": "…", "detail": "…", "violation": {…}}. error is the sentence for people, detail is optional, and violation names the refusing policy decision when there is one. A refusal by openshell.admin starts with "blocked by your organization's DefenseClaw policy".

StatuscodeWhen
400invalid_request, pack_invalidA malformed request, or a pack that fails to load.
403admin_violationopenshell.admin refused the request.
403policy_violationThe pack, the profile, or a DefenseClaw invariant refused it.
404not_foundNo such sandbox, approval, or route.
409conflictThe sandbox exists already or is in the wrong phase, another sandbox already mounts the folder (or one inside or around it) live, or an undo would revert a folder another running sandbox mounts.
409needs_copyThe project cannot be mounted live (a linked worktree, .git as a symbolic link, a git directory outside the folder, or a gateway whose compute driver mounts no host folders, when the request staged no copy); detail says why. sandbox run then creates the sandbox in copy mode.
409image_unavailableNo hook-checked harness image, and building one was refused or failed.
422policy_rejectedOpenShell rejected the sandbox configuration, or the sandbox it made does not run as DefenseClaw prepared it (the check after ready on the MicroVM driver: the identity the workload runs as, its capabilities, and DefenseClaw's hook and settings files). The sandbox is deleted, or stopped again after a start.
429unavailableToo many activity streams are open.
502upstream_errorAn OpenShell call failed.
503disabled, unavailableSandboxes are off, or the runtime or the OpenShell gateway is not available, or (MicroVM driver) the volume of its image cache lacks the room to prepare the disk of an image the sandbox is the first to boot: the message names what is free and what is needed.
504unavailableThe request was cancelled or timed out.
500internalAnything else.

Sandbox listeners

A sandboxed agent cannot connect out directly. The host-networked OpenShell supervisor carries the connections its policy allows, and it relays connections to host.openshell.internal to host loopback, so every sandbox request reaches DefenseClaw from 127.0.0.1. Neither listener trusts loopback for that reason: each request must carry a credential issued to one sandbox. Inside the sandbox, both listeners are addressed as host.openshell.internal:<port>.

ListenerConfig keyDefault portListens on
Hook ingressopenshell.ingress_portgateway.api_port + 1 (18971)127.0.0.1 only, never the main API address
Egress proxyopenshell.egress_portgateway.api_port + 2 (18972)127.0.0.1 only

The gateway starts both listeners, and the sandbox manager behind the Sandbox API, with the API when openshell.enabled is true on Linux or macOS. In a managed_enterprise install it starts neither, and /health reports the sandbox subsystem as disabled. A listener that cannot start marks the subsystem degraded (sandbox hooks then fail closed), but never stops the API.

The gateway checks the openshell: block every time it loads the config and refuses a config that fails; the error starts with config: openshell:. An explicit port that is not between 1 and 65535, or both keys set to the same port, always fails. When openshell.enabled is true, the effective ports must also be at most 65535 and differ from gateway.api_port, guardrail.port and each other. On a config reload, turning openshell.enabled on or off, or moving either port while it is on, restarts the main API listener. A sandbox keeps the ingress port its image and policy were built for, so re-create existing sandboxes after moving it.

Hook ingress

The hook ingress (internal/gateway/api_sandbox_ingress.go) is a second API listener that serves only what a sandboxed harness needs. Each sandbox has a binding: a record of its credential (stored only as a SHA-256 hash), the one connector it runs, and the route classes it may call. By default a binding may call its hook route and OTLP, plus notify for Codex; inspect is added only when asked for.

Route classPathsExtra condition
HookThe connector's hook route, for example POST /api/v1/claude-code/hook or POST /api/v1/codex/hookThe route belongs to the binding's connector.
NotifyPOST /api/v1/codex/notifyThe binding is for Codex.
InspectPOST /api/v1/inspect/{tool,request,response,tool-response}X-DefenseClaw-Connector names the binding's connector.
OTLPPOST /v1/{logs,metrics,traces}X-DefenseClaw-Source, when sent, names the binding's connector.

For an authenticated sandbox, every other path returns 404. That includes all management routes and the /otlp/{source}/{token}/v1/{signal} path-token form, which would put a credential in the URL.

Authentication. A request must send exactly one Authorization: Bearer dcsb_… header with the sandbox's binding credential. The master gateway token, connector hook tokens and OTLP path tokens are never accepted here. The sandbox manager mints the credential when it creates the sandbox and revokes it when the sandbox is deleted. openshell.token_delivery says how the credential reaches the sandbox:

  • provider (the default): as an OpenShell provider credential in DEFENSECLAW_SANDBOX_TOKEN. The sandbox sees only a placeholder, which OpenShell swaps for the real value on the way to the ingress. Each ingress listener has its own OpenShell provider profile, defenseclaw-ingress-<ingress_port>, so DefenseClaw daemons on different ports never re-point each other's sandboxes. The credential is rotated every time the sandbox starts, so a credential from an earlier session stops working.
  • env: as a plain DEFENSECLAW_SANDBOX_TOKEN variable. The agent can read the real value, and it is kept for the sandbox's life, because OpenShell can't change a sandbox's environment after it is created. The sandbox's policy then gets its own rule to reach the ingress.
ResponseWhen
401The credential is missing, malformed, unknown, expired or revoked. Auth-failure events are rate-limited to about ten per second, because every sandbox shares one source address.
400The credential also appears somewhere other than Authorization (another header, the host, the path or the query), where it could be echoed back or written to audit records.
403The route exists, but the binding may not call it, or the request acts for another connector.
429The sandbox is over its budget. The response carries Retry-After: 1.

Default per-sandbox limits.

TrafficRateBurstConcurrent requests
Hook, notify and inspect25/s10032
OTLP10/s504 per sandbox, 16 across all sandboxes

Hook and inspect bodies are capped at 1 MiB, like the main API. OTLP bodies are capped at 4 MiB, far below the main receiver's 64 MiB.

Other differences from the main API.

  • Identity comes from the binding: records are attributed to the host user who launched the sandbox. Identity headers such as X-User-Id are dropped.
  • Client-supplied request IDs are ignored. The gateway always mints the X-DefenseClaw-Request-Id it returns.
  • A hook or notify post may send X-DefenseClaw-Hook-Idempotency-Key. A retry with the same key and the same request within two minutes gets the original response, marked X-DefenseClaw-Idempotent-Replay: true, instead of a second evaluation. The same key on a different request gets 422.
  • A blocked tool call's verdict carries a plain reason for the agent: the rule and what to do instead, never the matched content.
  • The CSRF rules are the same as on the main API.

Egress proxy

The egress proxy (internal/openshell/egress) is the sandbox's general path to the web. The harness launcher and the sandbox's shells set HTTPS_PROXY and HTTP_PROXY to http://host.openshell.internal:<egress_port>, with a per-sandbox username and password in the URL. NO_PROXY lists host.openshell.internal and the model provider hosts, so hook traffic and LLM traffic do not go through the proxy. The proxy refuses a sandbox whose profile is strict, even one raised to strict after it was created: such a sandbox has no web egress.

Authentication. Clients send the credential as Proxy-Authorization: Basic. A request without it, or with a wrong one, gets 407 Proxy Authentication Required with Proxy-Authenticate: Basic realm="DefenseClaw egress". The proxy keeps only SHA-256 digests of the passwords, and issuing a new credential for a sandbox revokes the old one. The credential does not grant extra reach: it attributes each connection to a sandbox and applies that sandbox's policy and limits.

What it forwards.

  • CONNECT tunnels for HTTPS. TLS is never terminated, so certificate pinning keeps working. The proxy reads only the server name in the TLS handshake and closes a tunnel whose name it would block.
  • Absolute-form http:// requests, with hop-by-hop headers and the proxy credential removed.
  • Only destination ports on the sandbox's port list: 80 and 443 in the open and balanced packs, replaced by openshell.egress.ports when you set it. On 80 and 443 the proxy relays only web traffic; other protocols such as SSH pass only on ports you add.

What it decides. Each sandbox is decided by its own resolved policy: its pack, your openshell keys, its run options, and the admin constraints. Another sandbox's pack, block list, ports, or unblocks never apply to it. The unblock and approval checks and the triage of OpenShell connection requests ask the same decider, so they never disagree with the proxy. In short:

  • This machine is never reachable: loopback, its own addresses and names, link-local, cloud metadata, and reserved addresses.
  • Private networks are reachable only when an allow entry names them: an openshell.egress.allow entry, the pack's egress.allow, or openshell.admin.egress_allow_only. An unblock never opens them. With openshell.admin.allow_unblock: false, your own entries and those of a custom pack you picked are ignored, so only egress_allow_only or a required pack's list can open one; the how_to_unblock hint says so.
  • openshell.admin.egress_block (a host name there covers its subdomains), a host outside openshell.admin.egress_allow_only, and the block list (the pack's and openshell.egress.block) are refused, and can't be unblocked.
  • Under open, host names are allowed unless the built-in feed of exfiltration and abuse destinations lists them (paste sites, file drops, webhook catchers, tunnels, anonymizers). A destination given as an IP address is blocked, because it would get around the name-based feed. An unblock, or an allow entry that names it, lifts the block.
  • Under balanced, only hosts on the allow list are reachable. The rest can be unblocked.
  • Names are resolved by the proxy, and each answer is checked just before the connection, so a DNS answer that changes between check and connect cannot redirect it.

The full order, with what an allow entry, an unblock, and openshell.admin.allow_unblock: false change, is under How a destination is decided. POST /api/v1/sandbox/egress/unblock refuses what can't be unblocked: the administrator's lists and allow_unblock: false answer admin_violation ("blocked by your organization's DefenseClaw policy"). The block list, private networks, this machine, and the strict profile answer policy_violation.

A blocked request gets 403 with a JSON body: error: "egress_blocked", a message, the host, port, category and reason, the matching rule or feed entry, whether it is unblockable, and a how_to_unblock sentence written for the agent to pass on to you, such as the defenseclaw sandbox unblock command to run.

Policy changes reach open connections too. When a sandbox's policy changes (an administrator's constraint, for example), the proxy decides its open tunnels and in-flight requests again and closes the ones it now blocks. When a sandbox loses its proxy credential (its policy can no longer be resolved, or it moves to strict), all of its open tunnels close.

The proxy also counts bytes per destination and reports an upload of more than the configured threshold to a destination the sandbox had not contacted before. The threshold is the sandbox's own: openshell.egress.large_upload_mb, or else the egress.large_upload_mb of the pack the sandbox resolved to (25 MiB in open, 10 MiB in balanced; 0 in a pack turns the report off). It follows configuration changes with the rest of the sandbox's egress policy. The report is an egress.large_upload activity event and a finding; the upload itself is not cut off.

With the large-upload block on (the pack's egress.block_large_uploads, openshell.egress.block_large_uploads, or openshell.admin.block_large_uploads), the proxy also cuts the tunnel or request before the chunk that crosses the threshold, and answers the sandbox's later requests to that host (or to other first-seen hosts under its domain or at its address) with a 403 of category: "large_upload" whose reason names the threshold. The cut is an egress.blocked activity event instead of egress.large_upload: category is large_upload, reason is the proxy's sentence ("This sandbox tried to send more than 10 MiB to a destination it had not contacted before."; a later refusal, of a request that may send nothing, says "This destination is blocked since this sandbox tried to send more than 10 MiB to it, a destination it had not contacted before."; a report without the block says "More than 10 MiB was sent to a destination this sandbox had not contacted before."), bytes_up is what reached the destination (at most the threshold), severity is HIGH, and unblockable says whether POST /api/v1/sandbox/egress/unblock lifts the block for the host. Each later refusal is an ordinary egress.blocked event with that category. Hosts an unblock, an allow entry or openshell.admin.egress_allow_only names are exempt: their uploads are only reported.

Default per-sandbox limits. 256 open connections, 256 concurrent tunnels or requests, and 50 new tunnels per second (burst 200), with 1024 connections across all sandboxes.

Headers

HeaderDirectionPurpose
X-DefenseClaw-Token, Authorization: Bearer ..., or X-DC-Auth: Bearer ...requestMaster gateway authentication. Every route except GET /health requires either this credential or a route-appropriate scoped token.
X-DefenseClaw-ClientrequestCSRF marker — any non-empty caller identifier (cli, openclaw-plugin, inspect-hook/1.0). Required on non-GET/HEAD requests except the constrained loopback path-token OTLP case described above.
X-DefenseClaw-ConnectorrequestOn loopback generic inspect routes, selects the registered connector whose scoped hook token is being presented.
X-DefenseClaw-SourcerequestOn loopback /v1/{logs,metrics,traces}, selects and must match a header-scoped Codex or Claude Code OTLP token.
X-DefenseClaw-Request-Idrequest (optional), responseCaller-supplied correlation id. The gateway also accepts X-Request-Id and X-Correlation-Id; if none is supplied, it mints one. The chosen ID is always returned as X-DefenseClaw-Request-Id. The hook ingress ignores caller-supplied IDs and always mints one. See requestctx.go.
X-DefenseClaw-Hook-Idempotency-Keyrequest (optional)Hook ingress only: a retry-stable key on a sandbox hook or notify post.
X-DefenseClaw-Idempotent-ReplayresponseHook ingress only: true when the response replays an earlier one for the same key.
Last-Event-IDrequest (optional)Sandbox activity stream only: resume after this event sequence number. It wins over ?since.