Safemode investigations
claude-gateway safemode opens the real Claude Code terminal UI in a private investigation workspace. The gateway continues running. Codex is also supported:
claude-gateway safemode --name voice-debug --prompt "Inspect gateway session 11111111-2222-4333-8444-555555555555"
claude-gateway safemode --cli codex --name codex-debug --model YOUR_MODEL
claude-gateway safemode --resume voice-debug--resume selects a native Claude Code/Codex session ID or saved name, not a gateway chat session. Give a gateway chat session ID at any time: in the initial prompt, or later in the conversation. Safemode has full read access to the live gateway data and the bootstrap tells the CLI where it is (config, every agent's orchestration.db/history.db, logs, native transcripts), so the CLI looks the ID up directly. No refresh or relaunch is needed. IDs in a prompt also target the starting snapshot.
To resume interactively without sending the investigation introduction again:
claude-gateway safemode --resume voice-debug --no-bootstrap
claude-gateway safemode --resume voice-debug --no-bootstrap --prompt "Inspect the latest error"--no-bootstrap sends no automatic user message and starts no model turn by itself. An explicit --prompt is still sent. Diagnostics and INVESTIGATION.md refresh normally, but the model is not automatically asked to read them. Existing conversation instructions remain in native history. The flag is per invocation, requires an existing native conversation, and is unavailable for new sessions. It also works with safemode send NAME --prompt TEXT --no-bootstrap and the Gateway-managed task option gateway_target.no_bootstrap. A headless send always sends its explicit prompt. Omit the flag to restore the investigation introduction.
Models and config
Add an optional top-level safemode section to the gateway config:
{
"safemode": {
"cli": "claude",
"claude": { "model": "inherit" },
"codex": { "model": "inherit" },
"allowedAgentIds": []
}
}The resolved target config path is saved with the investigation, including through agent takeover. Use an explicit --config to change it.
The model selection order is command override, saved investigation selection, safemode config, native CLI default. First launch defaults to Claude Code and inherit. Inherit deliberately follows the native CLI's current configuration; it does not pin an unknown concrete model. Choose an explicit model to pin it across resumes. Gateway conversation/worker model settings are independent. A native conversation cannot switch between Claude and Codex.
Native authentication must already be configured. Codex uses its own native provider/login, including ChatGPT and keyring authentication; it does not borrow Claude credentials. Default launches check local native configuration/auth without a model request. Missing login reports codex login or --cli claude; resumed sessions never switch harnesses. Explicit interactive --params is operator-controlled and lets the native CLI validate login itself. Safemode still forwards the selected native provider’s env_key and environment-backed headers, resolving a named profile before CLI -c overrides. GATEWAY_CONFIG and GitHub CLI credentials (GH_TOKEN, GITHUB_TOKEN, GH_HOST, GH_ENTERPRISE_TOKEN) are forwarded so safemode can repair and publish issues; gateway API tokens and unrelated provider credentials are not. Local readiness does not prove network access or quota. This implementation uses Claude Code's --safe-mode / --permission-mode bypassPermissions and Codex's exec resume / --ignore-rules; older CLIs without these flags fail. Tested with Claude Code 2.1.274 and Codex 0.154.0. Claude headless inheritance preserves native authentication settings and the user's model setting, without importing executable customizations skipped by --safe-mode.
Local controls and takeover
claude-gateway safemode list --json
claude-gateway safemode status voice-debug --json
claude-gateway safemode send voice-debug --takeover --request-id investigate-1 --prompt "Inspect the latest interruption" --json
claude-gateway safemode status voice-debug --request-id investigate-1 --json
claude-gateway safemode logs voice-debug --json
claude-gateway safemode stop voice-debug
claude-gateway safemode --resume voice-debug --takeover
claude-gateway safemode delete voice-debugsend starts headless execution in the background; --wait runs it in the foreground. One owner is allowed per investigation. Without explicit --takeover, an active owner returns BUSY. A send can take over an interactive owner; an already running headless request remains busy and must be explicitly stopped first. Takeover announces the stop in the existing terminal, sends graceful termination, waits for exit, and then resumes the same native conversation. Stop requests carry the observed owner token; if ownership changes before the request arrives, the replacement owner refuses it. A timeout does not authorize starting a second owner or killing an unrelated process.
Safemode also checks native ownership outside its own lock: Claude's live session registry and native process evidence, plus Codex writer-lock/rollout descriptors. list and status include nativeOwners, or an explicit ownership-verification error. An existing external owner blocks launch even when no safemode owner is recorded. External processes are never terminated by --takeover; close them through their own terminal first.
On Linux, ownership is checked before preparing diagnostics, immediately before launch, and every second while running. If another owner is positively identified, safemode requests termination of its own child only and marks its request failed. Different native config homes and unrelated registered sessions are kept separate. Platforms without the required /proc evidence are refused rather than treated as free. A same-user native process with protected /proc metadata, including a container process, can also prevent verification even when it appears unrelated. Safemode reports the PID and refuses a new launch; it never stops the external process. After a session has started successfully, an inspection failure produces one warning and ownership checks continue without killing the active session. A detected competing owner still stops the safemode-managed child. Monitoring cannot establish exclusive ownership while process metadata is inaccessible.
External CLI limitation: Claude Code 2.1.278 permits concurrent --resume of the same native session. Safemode cannot make an arbitrary external Claude CLI honor its lock, so the monitor cannot guarantee zero transient overlap or immediate termination if its child ignores the signal. Codex 0.154.0 rejects concurrent writers through its native writer lock; older versions without that behavior are not a basis for an atomic-exclusion guarantee. Strict exclusion across every possible launcher requires a shared native lock respected by those launchers. Safemode's own launches remain serialized by its private ownership lock.
Request IDs prevent duplicate work. Reusing an ID returns its recorded outcome; changing its prompt/model/bootstrap option is rejected. After an unexpected crash, a running request's outcome can remain unknown: inspect it before choosing a new ID. Safemode has no internal work queue. The caller owns scheduling, dependencies and retries; Gateway-managed tasks provide this tracking for agent-originated work.
The final native answer is retained separately in each request receipt (up to 262,144 characters; larger answers fail explicitly). Headless output retains the most recent bytes (up to 5 MiB), so verbose intermediate tool output does not discard the final diagnosis. The logs command returns the latest 64 KiB.
Private state, request receipts and bounded headless output live under ~/.claude-gateway/safemode. Interactive output remains in the native terminal/history. delete removes safemode-owned artifacts, not the native provider's conversation history. If a supervisor crashes, recover NAME clears stale ownership only after both recorded processes have exited. It never signals a PID recovered from disk. An unresponsive live owner must be dealt with manually before recovery.
Agent control
Trusted host agents discover assigned investigations through capabilities_list and send work through Gateway-managed tasks. The gateway tracks the request and wakes the conversational agent when a result is available. It does not launch a model worker just to poll safemode. The five standalone safemode_* agent tools are no longer loaded.
Access requires both an allowlisted agent and an explicit assignment of the native session to that agent. Unassigned sessions are local-operator-only. An agent cannot discover or target another agent's session, even when it knows the native ID. App/container agents and workers cannot use these host controls.
First allow the host agent in config.json:
{
"safemode": {
"allowedAgentIds": ["claude-founder"]
}
}Then create an investigation interactively and assign it from the local terminal while it is idle:
claude-gateway safemode assign astra2 --agent-id claude-founder
# Remove agent access without deleting native history:
claude-gateway safemode assign astra2 --revokeAssignment changes require local CLI access; agents cannot assign themselves sessions. Assignment is stored separately from native history and refuses changes while a safemode owner is active. Allowlist changes hot-reload. Discovery and dispatch check current permissions, including queued work. An existing committed request remains trackable/cancellable by its originating task after revocation; revocation does not terminate that request or expose subsequent requests.
When asked “Can you see astra2?”, the agent calls:
{"scope":"safemode","query":"astra2"}This is a capabilities_list call, not a task. It returns assigned session names, native IDs, CLI, model and ownership status. A known foreign ID and an unknown ID are both unavailable; knowing an ID grants no access.
To send authorized work, use task_spawn:
{
"title": "Inspect the stalled conversation",
"instructions": "Investigate the requested session and return evidence and a proposed fix. Do not restart.",
"target_profile": "gateway-managed",
"gateway_target": {
"adapter": "safemode",
"session_id": "NATIVE_SESSION_ID",
"takeover": true,
"no_bootstrap": true
}
}takeover requires explicit user authorization to stop an interactive owner. Another headless request is allowed to finish first. no_bootstrap omits the investigation introduction; snapshots still refresh. Text instructions are supported; attachment references, worker skills and working-directory overrides are rejected rather than silently ignored. Start with an existing native conversation; the task does not create a new investigation.
The task is visible in /tasks, CLI/API task views and the dashboard as Gateway-managed · Safemode · astra2. Use task_status for the full retained result and task_cancel to cancel only this request, never a newer owner. Agent task access is scoped to its conversation, not just its agent ID. Dashboard/admin access retains its existing administrator scope.
Request IDs and dispatch intent are persisted before submission. A gateway restart resumes observation without resending the command or stopping the independent safemode process. Ambiguous submission or lost ownership becomes needs_reconciliation; it is never blindly retried. Successful native exit without a recorded final answer is an explicit failure, not a fabricated completion report. Results are saved per request; a later request cannot overwrite the prior task's report. External native token/tool usage is not measured as a gateway worker turn, so the dashboard does not invent worker telemetry.
Queued instructions may be amended. Once sent, use a dependent Gateway-managed task with continue_task_id for an authorized follow-up; it waits for successful completion by default. Existing task queue limits, membership checks and automatic result delivery apply. Local CLI controls remain available while the gateway is down; channel reporting resumes when the gateway returns.
Evidence and permissions
Safemode exists to diagnose and repair the gateway, so it has full, unrestricted access to the host. Claude Code runs with --permission-mode bypassPermissions (interactive and headless, every built-in tool, no permission prompts). Codex runs with sandbox_mode="danger-full-access", approval_policy="never" and a shell that inherits the filtered environment. Claude Code refuses bypassPermissions when run as root unless IS_SANDBOX=1 is set (for example in a container); safemode forwards IS_SANDBOX, otherwise run it as a non-root service user. Safemode can read any file, query the live databases, run commands and change config, data or services. The bootstrap asks it to back up before writing, to stop the gateway or confirm it is idle before writing a live database, and to report every change it makes. Trusted agents that send work to an assigned investigation (see above) therefore delegate to a full-access host session; assign sessions accordingly.
Each run also writes diagnostics/locations.json with the absolute live paths (config, agents directory, every agent and its database files ordered by recent activity, log directory, Claude Code and Codex transcript directories) plus a bounded redacted starting snapshot: config/log/SQLite excerpts, health evidence and startup/build provenance. The bootstrap includes those paths and the ID lookup recipe (conversations, tasks, task attempts, agent_cli_sessions, messages, per-session log files, native transcripts), using Node's built-in node:sqlite in read-only mode because the sqlite3 CLI is often absent. When a prompt contains session or task IDs (UUIDs, or prefixed IDs such as cron-… and telegram:<chat> that exist in an agent database, so model names or dates are not mistaken for IDs), database collection searches agent directories for matching evidence before applying the eight-agent snapshot limit. Unrelated databases are omitted; coverage lists selected and omitted agents with reasons, as well as IDs beyond the ten-target limit. Targets persist when resuming without a new target; supply a new ID in --prompt to switch investigations. Without a target, the snapshot covers the eight most recently active agents and coverage explicitly identifies the general snapshot. Each target ID that matched no database is named in coverage.json notes. The bootstrap uses workspace-relative paths such as diagnostics/provenance.json for the snapshot, without embedding a session ID. Claude launches with that workspace as its process cwd; Codex also receives an explicit --cd on fresh and resumed launches so a remembered conversation directory cannot redirect evidence reads. Source snapshots come from 0xMaxMa/claude-gateway at the recorded build revision. Build commit and startup checkout state are separate evidence: moving a checkout after launch does not change the recorded build. Dirty builds, legacy builds, stale startup records and unavailable evidence are explicitly marked uncertain. A release tag is only an inferred reference unless exact build evidence exists. Main is never substituted as the running source.
Collecting the snapshot itself never resets the running checkout, executes fetched source, takes the gateway's process lease, or writes a live database. The native CLI starts without the possibly broken native customizations: Claude Code uses --safe-mode and --strict-mcp-config; Codex disables external MCP servers, hooks, legacy notification programs (notify), plugins, app/browser tools, native image generation and automatic skill/dependency installation. These keep a broken native configuration from interfering; they do not limit what safemode can read or change.
Ask for a diagnosis with evidence, hypotheses, proposed repair and regression tests. To file an issue, explicitly request it: the CLI searches duplicates, prepares sanitized English content and publishes with authenticated gh. Automatic redaction is best effort; inspect content before publishing. Collecting diagnostics alone never merges, deploys, restarts the gateway or publishes issues; the CLI does those only as part of the work you ask for.
Native interactive parameters
Use --params for native Claude Code or Codex options from your terminal:
make cli ARGS="safemode --cli codex --params '--dangerously-bypass-approvals-and-sandbox resume 11111111-2222-4333-8444-555555555555'"
make cli ARGS="safemode --resume investigation-name --params '--model gpt-5'"Safemode splits the quoted value into arguments without shell evaluation, variable expansion or command substitution. Your calling shell and Make still process their own quoting; do not interpolate untrusted text into a shell command.
Explicit parameters replace the default interactive permission/MCP isolation options with the native CLI's configuration and your supplied options. Provider credentials still use the filtered environment. Without --params, existing interactive defaults remain unchanged. Parameters are not saved or reused by headless requests. send and the MCP tools do not accept this option.
Outer --resume selects a saved safemode conversation by its native session ID or name. Native resume UUID (Codex) or --resume UUID (Claude Code) inside --params attaches that exact native conversation to a new investigation. Combining the two is an error. If an investigation already owns that native ID, use its outer --resume. A native process already using the conversation must exit before attachment.
Session identity, noninteractive execution, remote endpoints and working-directory switches are not passthrough options: safemode needs a locally trackable interactive conversation. Picker/--last, continuation and fork modes are unsupported. Use --prompt for initial prompt text. Native flags are validated by the installed CLI; unsupported flags fail normally rather than being silently ignored.
One native session, one ID
Safemode uses the Claude Code or Codex session UUID as its only public session ID. The same UUID works with --resume, status, send, logs, stop, recover and delete. Names are optional aliases, not separate session identities.
Claude Code receives the UUID on its first launch. Codex assigns its own UUID: until its authoritative session metadata is available, list shows id: null, status: "starting" and a temporary name you can use to stop it. Once discovered, safemode publishes the native UUID. While the process is running, the workspace, owner lock and control socket stay in place. After it exits, the workspace directory is aligned with the native UUID. It never guesses the latest global Codex session.
Older investigations are exposed by their native UUID. Idle metadata migrates automatically; live legacy supervisors keep their files and ownership unchanged until they exit. After a managed native process exits, legacy and temporary workspace directories are moved to the native UUID under the ownership lock. Logs and request receipts are preserved. The old directory becomes a compatibility symlink so historical working-directory and file references still resolve; it is not a second session. Native conversation history is never rewritten. Active workspaces are never moved.
make stop uses the gateway CLI's manager-aware stop operation and no longer kills processes by command-line pattern. Safemode opened independently in another terminal remains running when the gateway stops or restarts. Closing that terminal, stopping its enclosing service/container, or rebooting the host still stops it.
Rename a safemode conversation
make cli ARGS="safemode rename 11111111-2222-4333-8444-555555555555 gateway-debug"
make cli ARGS="safemode --resume gateway-debug"The first argument accepts the native session ID or the current name. Names contain 1–64 letters, digits, dots, underscores or hyphens and must be unique. Renaming works while the native CLI is running; it does not stop or restart it, change its native ID, or move its workspace/history. The old name stops resolving, while the native ID continues to work. Rename changes the safemode alias, not the native CLI's own conversation title.
If a rename process is interrupted, close the native conversation and use safemode recover ID to clear a stale rename claim. Recovery refuses while either the conversation owner or rename owner is still alive.
Interpreting subprocess diagnostics
Claude Code subprocess exit records include the gateway response ID (agent turns), task/attempt IDs (workers), native message ID when available, model, turn outcome and exit reason. A completed turn followed by cleanup can exit with code 143; this is distinct from unexpected_exit without a terminal result. Cancellation, timeout and shutdown are recorded separately. Failed turns retain their failure code when available.
CLI stderr remains visible and correlated with these records. A model-name warning does not establish provider rejection: check the terminal inferenceOutcome and the recorded error. Success means the CLI reported a successful result, not that every earlier request succeeded. Missing terminal evidence remains unconfirmed.
A provider refusal is recorded from the stream's typed stop_reason: "refusal" and stop_details fields, never from response wording. The turn record lists each refused native message under providerRefusals (messageId, the provider's category/explanation only when supplied, and any toolCalls the refused message carried with result: true only when a tool result was recorded), plus the CLI's safetyStops count. A turn can be completed and still contain a refusal, for example when a fallback answer followed it.
Terminal appearance
Interactive safemode preserves the caller's terminal identification, tmux and color preferences rather than choosing a separate theme. Reopen the CLI after upgrading for environment changes to take effect; restarting the gateway does not change an already running safemode CLI.
When tmux reports a light background through OSC 11 while the attached terminal is dark, Codex can draw pale prompt boxes. Preserving environment variables alone cannot correct a wrong terminal color response. Before changing anything, compare direct Codex and safemode using the same binary version, terminal and tmux session.
With tmux 3.4, OSC 10/11 default-color queries use the first eligible attached client's colors. A light-themed code-server terminal attached earlier can therefore supply colors for a dark SSH client attached later. Inspect attached clients:
tmux list-clients -F '#{client_name} #{session_name} #{client_activity}'If you identify a stale client you no longer need, detach only that client with tmux detach-client -t /dev/pts/CLIENT_NUMBER. Detaching a client leaves the tmux session and its processes running. Do not detach clients that are still in use. Reopen the native CLI when idle to refresh its cached palette. For clients that intentionally use different themes, use separate tmux sessions. Safemode must not force a pane palette, detach clients automatically, or silently change the user's terminal theme. Default text colors such as green still follow the actual terminal's configured foreground.
Troubleshooting
| Symptom | Meaning and next step |
|---|---|
Agent receives ACCESS_DENIED | Inspect the structured reason below. This code alone does not prove an allowlist problem. Recheck the tool after changing configuration; a previous rejection is historical evidence. |
BUSY | An investigation already has an owner. Inspect status; use explicit --takeover only when you intend to stop its current interactive owner. Stop an active headless request first. |
| Native session already belongs to an investigation | Use safemode --resume NATIVE_ID; do not attach it again through native resume parameters. |
| Native ownership cannot be verified before launch | Inspect the reported PID and OS process visibility. Safemode refuses a new launch without sufficient evidence. recover does not bypass a live owner. |
| Ownership inspection warning during an active session | The session stays running and checks continue. This warning alone does not mean another owner exists. |
| Gateway logs mention a session but its database is absent from the snapshot | Give the ID to the CLI at any time; it queries the live databases listed in diagnostics/locations.json directly. The snapshot is only a bounded starting point; coverage.json lists selected/omitted agents and names target IDs that matched no database. |
diagnostics/... is missing after native resume | Use diagnostics/ relative to the active safemode workspace; current Codex launches explicitly set that directory. Native history may contain paths from an earlier working directory or snapshot. |
| CLI reports an unfamiliar model name | Check the correlated terminal request outcome and provider error. A warning accompanied by a successful result is different from a rejected request. |
| Colors differ from the normal terminal | Close and reopen safemode after upgrading so it inherits current terminal/color settings. Gateway restart alone does not update an existing native CLI. Compare in the same terminal and tmux pane; safemode does not override a native theme or repair terminal palette settings. |
delete refuses an investigation | Stop its owner first. Deletion removes safemode state and snapshots, not native CLI conversation history. There is no clear all command. |
Snapshot coverage
Snapshots are a starting point collected at launch or send time, not a live mirror; the live data itself is always readable at the paths in locations.json. Read coverage.json before drawing conclusions from the snapshot:
- Up to ten unique target IDs and eight matching agents. Agent directory order does not exclude a matching agent merely because eight unrelated agents precede it. Without targets, the eight most recently active agents are selected.
- Each database snapshot is capped at 1 MiB, 50 recent rows per table and 8,192 characters per text field. Tables without session columns can include recent rows unrelated to the target.
- Logs contain eight streams, with at most 128 KiB from each tail: per-session streams named after a target ID first, then the most recently modified.
- Five previous diagnostic snapshots are retained. New snapshots and saved target selection do not modify live gateway databases or native conversation history.
- A session ID supplied in a new safemode prompt replaces the saved target selection. A resume without new IDs retains it. An untargeted first launch is explicitly marked as a general snapshot.
Replace the example UUIDs with your gateway chat session ID when selecting evidence, and with your native CLI session ID when resuming a safemode conversation. They identify different things.
Authorization denial reasons
The private task bridge retains HTTP 403 and error: "ACCESS_DENIED", adding a safe reason field. MCP forwards this field unchanged, and gateway logs record the agent ID and reason without credentials or prompt text.
| Reason | Meaning |
|---|---|
SAFEMODE_AGENT_NOT_ALLOWED | The runtime's allowlist does not include this agent. Update the list and verify the next request after configuration reload. |
SAFEMODE_HOST_ONLY | An app/container agent cannot control host safemode. |
CONVERSATION_ACCESS_DENIED | The principal is not a member of the conversation. |
EXECUTION_NOT_AUTHORIZED | This turn may inspect but cannot send or stop. |
ADMISSION_DENIED | Execution admission rejected the requested mutation. |
TICKET_INVALID_OR_REVOKED | The bridge ticket is missing, invalid or revoked. This is not an allowlist verdict. |
COMPACTION_SCOPE | A compaction-only scope cannot call operational tools. |
INVALID_BRIDGE_REQUEST | The request method, path or origin is not permitted. |
Tasks waiting for a busy investigation
A Gateway-managed task waiting for its safemode target stays queued and does not consume an execution slot. Its instructions can still be updated, and cancellation does not send anything to the target. For additional instructions on the same goal, use task_update with mode: when_ready and the complete revised brief. The current request settles before the next revision runs under the same task ID. Completed or failed tasks can continue this way; cancelled tasks and requests with unknown outcomes cannot be restarted implicitly.
If both recorded owner processes have exited but their lock remains, the task fails with SAFEMODE_RECOVERY_REQUIRED instead of waiting indefinitely. The operator must inspect the investigation and run safemode recover ID before retrying. The task never removes ownership locks automatically.