Navigate the docs

For AI agents: the documentation index is at /llms.txt, and every page here has a markdown version at its URL plus .md.

Diagnostics: error codes and surfaces

This page lists SafeClaw error codes, response shapes, actions, and known blind spots. SafeClaw reports problems loudly and leaves unrelated upstream errors without a SafeClaw marker.

src/error.rs::ScCode is the single registry; this table is its rendering and changes in the same commit. Codes are snake_case and stable, so never renumber or reuse them.

Wire shapes (one row, three renderings)

SurfaceShape
Control port :23293 + API face :23294RFC 9457 application/problem+json: type (https://safeclaw.pro/errors/<code>), title, status, detail + extensions code, action, cause. Legacy error/message dual-emitted for one transition window.
Proxy MITM plane :23294text/plain (deliberately never parseable as the upstream's payload), body first line SafeClaw: <code>: <message>, header x-safeclaw-error: <code>. Captive bodies (approval / widen) keep their text+JSON-tail format and carry the code header.
CLI stderr<code>: <message> (cli/apierr.rs), same code the wire carried.

action = what the agent does next: unlock (have the user unlock), approve (follow the approve URL in the body), retry, configure (a knob, named in the message), fix_request, none (explicit refusal, do not work around it). cause = attribution: request/auth/vault/policy/config/internal, plus environment and upstream which are NOT SafeClaw problems, reported by SafeClaw because it is the hop that observed them.

Codes

codestatusactioncausemeaning
bad_request400fix_requestrequestmalformed request
unauthorized401configureauthmissing/invalid credential on a control/API call
forbidden403noneauthauthenticated but not allowed
not_found404fix_requestrequestno such resource
method_not_allowed405fix_requestrequestAPI face is GET-only
conflict409fix_requestrequeststate conflict
vault_locked423unlockvaultvault has no in-memory key; canonical hint: run sc up
vault_access_revoked403configureauththis account is no longer a member of the vault; ask a vault admin to add it back
vault_deleted410nonevaultthis vault was deleted
vault_key_changed409unlockvaultthe vault was re-keyed after its members changed; approve sc unlock to read rows sealed under the new key
rate_limited429retryrequestslow down
internal500retryinternaldaemon bug/failure
ca_unavailable500retryinternalresident CA unreadable
agent_key407configureauthphantom-bearing request without a verified agent identity (run the agent through sc run; set it up with sc setup --name <name> or sc agent add)
agent_not_admitted403configureauththe agent proved its identity, but the account's ledger does not admit it (never approved, or revoked); refused on every vault, whatever its Agent access page lists. Fix: run sc setup --name <name> or sc agent add
agent_not_in_account403configureauth(cloud op relay) the agent isn't registered to the account this machine is signed in to. The daemon itself answers agent_not_admitted. Fix: run sc setup --name <name> or sc agent add on this machine
ambiguous_phantom400fix_requestrequestbare phantom on a multi-secret connection; use a role phantom
approval_needed401approvepolicypolicy asks; body carries approve/poll URLs + op id
approval_register503retryinternalcould not open the approval op
approver_not_authorized403noneauththe passkey that approved is neither the requester nor an admin of this shared vault; the request stays pending
wk_refused_v2400nonerequestan approval delivered an unlock secret instead of the vault key and the approver's signature; an approver's identity key never leaves their browser, so the daemon refuses it. The request stays pending; reload the approval page (it needs the current console)
grant_id_invalid403noneauththe approval's identity signature is missing, does not verify, or names someone other than the owner of the passkey that approved; the request stays pending
device_not_admitted403configureauththis device has no valid owner-signed admit over its device and daemon keys, so no approval can be sealed to it. Fix: run sc setup --name <name> or sc login
ledger_stale423retryenvironmentSafeClaw could not pull a verified device list from your account for 7 days; vaults are locked (data stays) until it reconnects
ledger_device_revoked403configureauththe account owner revoked this device (a verified revoke in the device list; a newer admit of a re-paired device follows its revoke). The daemon latches it. Fix: run sc setup --name <name> or sc login to pair again
ledger_head_invalid403noneauththe account's device list does not match its owner-signed head (a forged or truncated list); the daemon keeps its previous view
broker_body_limit413configureconfigbody exceeds the inspectable cap; raise --body-cap if legitimate
egress_unreachable502configureenvironmentforward hop failed to connect (NOT a credential problem)
exposes_unsupported400fix_requestrequestrole not mintable yet
host_forbidden403nonepolicydestination fails the private/metadata egress floor
host_not_anchored403approvepolicyconnection not anchored to this host; body carries a one-tap widen
multi_connection400fix_requestrequestone request named several connections
no_vault403configureconfigno vault bound to the request
oauth_mint502retryupstreamtoken mint failed at the provider
phantom_plain_http400fix_requestrequestphantom requires HTTPS
policy_denied403nonepolicyblocked by an explicit deny rule
refresh_forbidden403fix_requestpolicyrefresh tokens never leave the vault
secret_encoding500noneinternalresolved credential not valid UTF-8
store_unavailable502retryupstreaman external secret store (e.g. GCP Secret Manager) failed while resolving; daemon logs have the full error
unknown_connection400fix_requestrequestphantom names no known connection
upstream_body502retryupstreamrequest body could not be read
upstream_error502retryupstreamupstream answered but the exchange failed (NOT a credential problem)

CLI exit codes: 0 ok, 1 error, 2 usage; sc op wait: 0 approved, 3 expired or unknown, 4 timed out waiting, 5 not approved (a person said no, or a stated cause), 6 ended without a decision (withdrawn by the requester or superseded by a newer request). sc doctor exits non-zero if any check fails.

Op terminal states: the poll body's status stays pending | ok | consumed | rejected; a rejected op carries reason, and waiters distinguish a USER rejection from a withdrawal by the requester or a newer request replacing it: those say so and ask for a re-run, never "rejected". First-class wire statuses (cancelled / superseded / expired) need the console's chips to change in the same release: parked as follow-up.

Blind spots (the broker cannot yell here)

  1. Not in the path: the process wasn't routed (no sc run / env bundle): the phantom reaches the upstream literally. Upstreams reject (401) or silently degrade; either way there is no SafeClaw marker. The phantom string itself (__sc__*__) is the greppable brand; the skill's routing check covers this.
  2. Locked before ever unlocking (this boot): anchor hosts live in the sealed blob, so a daemon restarted while locked cannot recognize brokered hosts and blind-tunnels them. Once a vault has been unlocked once, its anchors are remembered across Lock (state.last_host_unions) and a phantom sent while locked gets an explicit vault_locked instead.

Invariant for new proxy branches: every outcome is either passthrough (not ours) or a registry-coded error: no third state, no silent drop.