cyberine
Providers

Claude Code CLI (headless)

Default model: claude-sonnet-5-5 (CYBERINECLAUDEMODEL; was claude-sonnet-5 until 2026-10-01, kvhxs). Auth dir allow-listed by the sandbox: ~/.claude.

  • Kind: headless coding agent (subprocess)
  • Official docs: https://code.claude.com/docs/en/cli-reference · /headless · /authentication
  • Cyberine adapter: CliAgentAdapter via makeClaudeAdapter (build worker claude)
  • Researched/verified: 2026-07-13 — confidence high. Updated 2026-07-17 (T622/T624/T626/T628/T629): makeClaudeCodeAdapter (the unreferenced --bare read-only variant) removed as dead code — grep confirmed nothing outside its own definition called it, and buildAdapters.ts has no case 'claude-code':. makeClaudeAdapter gained --exclude-dynamic-system-prompt-sections (unconditional on the default-prompt path), an opt-in --effort dial, opt-in --forward-subagent-text capture, and opt-in --json-schema structured output — see "Key flags" and "Cyberine env vars" below. Updated 2026-07-23 (T646, v2.1.212 headless deltas, live cross-checked against the installed v2.1.218 binary via claude --help + strings): CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION / CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION (new per-session caps, default 200 each) now have opt-in Cyberine overrides — see "Cyberine env vars". SIGTERM-in-print-mode now exits 143 cleanly (no orphaned tree); confirmed CliAgentAdapter's cancel path already classifies any exit following its own SIGTERM as 'cancelled' regardless of the numeric code, so no code change was needed there. Transcripts now carry a reasoning-effort field; confirmed parseClaudeStreamJson/parseClaudeSessionTree pick named fields off each event and ignore unknown ones, so the new field is inert by construction. CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS is documented as a no-op today (no claude build worker is ever given --mcp-config, the T488 MCP-bridge gap is still open). Updated 2026-08-20 (T656, live-verified against the installed claude v2.1.237): the T626/T628 opt-ins are now reachable via CYBERINE_CLAUDE_CAPTURE_SUBAGENT_TEXT / CYBERINE_CLAUDE_OUTPUT_SCHEMA; unknown-flag rejections from an older claude (error: unknown option '--flag', exit 1) surface as named flag-compat errors (see integration notes). Updated 2026-08-27 (T627, live-probed and adopted against the installed v2.1.246): --no-session-persistence is now applied on the cold-spawn path whenever !opts.warmSession && !opts.captureSessionTree -- see "Key flags" and "Gotchas" below for the probe evidence. Updated 2026-08-29 (T695, three headless deltas filed by the 2026-08-28 R02 sweep, cross-checked against the raw CHANGELOG.md and live-verified against the installed v2.1.251 binary): all three confirmed harmless, no adapter change needed -- see "Gotchas" below for the full evidence chain (SIGTERM no longer recording an interrupted turn / synthetic tool denials, MCP headersHelper now requiring a trust-dialog accept, and sub-agent model-404 silently using the session's fallback chain).

Cyberine invocation

# build worker (makeClaudeAdapter), cold spawn
claude -p "<prompt>" --output-format stream-json --verbose \
  --model <model> --dangerously-skip-permissions --safe-mode \
  [--fallback-model <models>] [--thinking disabled] [--effort <level>] \
  [--system-prompt <persona> | --exclude-dynamic-system-prompt-sections] \
  [--forward-subagent-text] [--json-schema <schema>] \
  [--no-session-persistence] --add-dir <cwd>

# build worker, warm-session resume (opt-in via opts.warmSession)
claude --resume <sessionId> -p "<prompt>" --output-format stream-json --verbose \
  --dangerously-skip-permissions --safe-mode \
  [--fallback-model <models>] [--thinking disabled] [--effort <level>] \
  [--forward-subagent-text] [--json-schema <schema>] --add-dir <cwd>

# background fleet worker (makeClaudeBgAdapter, opt-in via CYBERINE_CLAUDE_WORKER_MODE=bg)
claude "<prompt>" --bg --name cyberine-w-<runId8>-<slot> --model <model> \
  --permission-prompts none --dangerously-skip-permissions --safe-mode \
  [--thinking disabled] [--effort <level>] [--system-prompt <persona>] \
  --settings '{"env":{...}}' --add-dir <cwd>

Default model: claude-sonnet-5-5 (CYBERINE_CLAUDE_MODEL; was claude-sonnet-5 until 2026-10-01, kvhxs). Auth dir allow-listed by the sandbox: ~/.claude.

--safe-mode and --dangerously-skip-permissions are applied unconditionally on every build-worker spawn (cold or resumed). --fallback-model / --thinking disabled / --system-prompt are conditional — see "Cyberine env vars" below.

Auth (precedence order)

  1. Cloud env: CLAUDE_CODE_USE_BEDROCK=1 / ..._VERTEX=1 / ..._FOUNDRY=1
  2. ANTHROPIC_AUTH_TOKEN → Authorization: Bearer (LLM gateways)
  3. ANTHROPIC_API_KEY → X-Api-Key (always used in -p mode when present)
  4. apiKeyHelper setting (dynamic key script)
  5. CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token, CI) — NOT read in --bare mode
  6. Subscription OAuth from claude auth login (macOS Keychain / ~/.claude/.credentials.json)

--bare does not read CLAUDE_CODE_OAUTH_TOKEN → use ANTHROPIC_API_KEY or apiKeyHelper with --bare.

Key flags

FlagPurpose
-p / --printNon-interactive (required for all headless flags).
--output-format text|json|stream-jsonstream-json = JSONL events (Cyberine uses this).
--verboseRequired with stream-json to get intermediate events; else only the final result.
--model <alias|id>Aliases: sonnet, opus, haiku, best, opusplan, sonnet[1m], opus[1m].
--dangerously-skip-permissions= --permission-mode bypassPermissions. Required headless; does NOT override deny rules.
--safe-modeIsolates the worker from the operator's global ~/.claude customizations (CLAUDE.md, skills, plugins, hooks, MCP, commands/agents, output styles, workflows, themes, keybindings); sets CLAUDE_CODE_SAFE_MODE=1. Does not affect auth, model selection, built-in tools, or permissions. Always passed by makeClaudeAdapter (T492).
--fallback-model <alias|id>[,<alias|id>...]Comma-separated fallback chain, tried on a primary-model failure; the CLI re-tries the primary at the start of each user turn. "Only works with --print" per claude --help. Opt-in via CYBERINE_CLAUDE_FALLBACK_MODEL (T494).
--thinking <enabled|adaptive|disabled>Help-hidden but real (commander option, v2.1.197). makeClaudeAdapter only ever passes disabled, opt-in via disableThinking / CYBERINE_CLAUDE_DISABLE_THINKING (T494) — the claude worker profile is tier strong, so thinking stays ON by default.
--system-prompt <text>Overrides the system prompt; Cyberine passes CYBERINE_SYSTEM_PROMPT only when config.persona === 'cyberine'.
--exclude-dynamic-system-prompt-sectionsMoves per-machine sections (cwd, env, memory paths, git status) out of the system prompt into the first user message, improving cross-user prompt-cache reuse. Only applies with the default system prompt (ignored with --system-prompt). Applied unconditionally by makeClaudeAdapter whenever config.persona !== 'cyberine' (T622).
--effort <low|medium|high|xhigh|max>Effort level for the session. Opt-in via opts.effort / CYBERINE_CLAUDE_EFFORT (T624); absent (default) = CC's own default for the model.
--add-dir <path>Extra file access; does not load .claude/ config from those dirs.
--resume <sessionId>Resumes a prior session (T333 warm-session reuse, opt-in via opts.warmSession); combined with -p for a follow-up headless turn. Falls back to a cold spawn when the resumed session exits non-zero.
--bareSkip hooks/skills/plugins/MCP/memory/CLAUDE.md (reproducible CI). Not currently used by any Cyberine factory (the former makeClaudeCodeAdapter read-only variant was removed as dead code 2026-07-17, T629).
--include-partial-messagesToken-level text_delta events (needs -p + stream-json).
--forward-subagent-textForwards subagent text/thinking as extra assistant/user messages tagged with parent_tool_use_id; only works with -p + --output-format=stream-json (always true here). Opt-in via CYBERINE_CLAUDE_CAPTURE_SUBAGENT_TEXT / opts.captureSubagentText (T626, wired T656); extracted into Result.raw.subagentText by parseClaudeSubagentText.
--allowedTools <...>Narrower alternative to skip-permissions (e.g. Bash(git diff *)).
--max-turns <n> / --max-budget-usd <n>Limits (print mode).
--json-schema <schema>JSON Schema text (not a file path, unlike Codex's --output-schema), passed straight through for structured-output validation. Validated JSON lands in the result event's structured_output field. Opt-in via CYBERINE_CLAUDE_OUTPUT_SCHEMA (inline JSON or a schema-file path, inlined to text in core/config.ts) / opts.outputSchema (T628, wired T656); extracted into Result.raw.structuredOutput by parseClaudeStructuredOutput.
--no-session-persistence"Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)" per claude --help (verified on the installed v2.1.246). Applied by makeClaudeAdapter's cold-spawn baseArgs whenever !opts.warmSession && !opts.captureSessionTree (T627, adopted 2026-08-27) — a cold-spawn worker is never resumed, so the on-disk transcript is pure clutter; a warm-session or session-tree-capture worker keeps it unset. Live-probed before adoption: session_id and the system/init topology fields still appear in the live stream-json output with the flag set — see "Gotchas".
claude auth login / setup-token / auth statusAuth management.

Cyberine env vars

These control makeClaudeAdapter's own flag/env injection — distinct from the CLI's native auth env vars in "Auth" above.

Env varEffect
CYBERINE_CLAUDE_MODELOverrides the default model (claude-sonnet-5-5) passed via --model.
CYBERINE_CLAUDE_FALLBACK_MODELComma-separated model chain passed straight through to --fallback-model. Absent (default) = no fallback.
CYBERINE_CLAUDE_DISABLE_THINKING1/true passes --thinking disabled. Defaults to unset (thinking stays on) — this worker's profile is tier strong.
CYBERINE_CLAUDE_EFFORTOne of low/medium/high/xhigh/max, passed through to --effort. Validated in core/config.ts; an unrecognized value is treated as unset. Absent (default) = flag omitted, CC picks its own default.
CYBERINE_CLAUDE_MAX_WEB_SEARCHES_PER_SESSIONT646: positive integer, translated into the CLI's own CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION env var (see table below). Validated in core/config.ts; a non-integer/non-positive value is treated as unset. Absent (default) = omitted, CC's own 200-per-session default applies unchanged.
CYBERINE_CLAUDE_MAX_SUBAGENTS_PER_SESSIONT646: positive integer, translated into the CLI's own CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION env var (see table below). Same validation/default contract as the web-searches knob above.
CYBERINE_CLAUDE_CAPTURE_SUBAGENT_TEXTT626 (wired T656): 1/true passes --forward-subagent-text and extracts the tagged messages into Result.raw.subagentText. Absent (default) = off.
CYBERINE_CLAUDE_OUTPUT_SCHEMAT628 (wired T656): JSON Schema — inline JSON (starts with {) or a schema-file path (read and inlined in core/config.ts) — passed as --json-schema text; the validated object lands in Result.raw.structuredOutput. Unparseable value/file is treated as unset.

Injected into the subprocess environment on every spawn (not CLI flags, not documented in claude --help; verified present in the installed binary v2.1.197 via strings, re-confirmed present in v2.1.218):

Env varValuePurpose
CLAUDE_CODE_DISABLE_BUNDLED_SKILLS1Belt-and-suspenders alongside --safe-mode: stops the spawned worker from resolving the operator's bundled/marketplace skills via /skill-name.
CLAUDE_CODE_RETRY_WATCHDOG1Enables the CLI's own retry watchdog so a stalled/hung request under this unattended headless spawn gets retried instead of hanging until the adapter's own timeoutMs kills the process.
CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSIONopt-in, from CYBERINE_CLAUDE_MAX_WEB_SEARCHES_PER_SESSIONT646 (v2.1.212 new cap, default 200 — verified via strings on the v2.1.218 binary). Only injected when the Cyberine knob is set; omitted (default) leaves CC's own 200-per-session default in force.
CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSIONopt-in, from CYBERINE_CLAUDE_MAX_SUBAGENTS_PER_SESSIONT646 (v2.1.212 new cap, default 200 — verified via strings on the v2.1.218 binary). Same opt-in contract as the web-searches cap above.

Models / aliases

claude-sonnet-5-5 (sonnet, Cyberine default since 2026-10-01; CC v2.1.284 made it the default Sonnet), claude-sonnet-5 (previous sonnet, legacy), claude-opus-5 ("now the default Opus model" per CC v2.1.219 release note, 2026-07-24 -- not independently verified against opus alias resolution on the installed binary), claude-opus-4-8 (previous opus; needs CC v2.1.154+), claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-5, claude-fable-5-1 (CC v2.1.257, 2026-09-04 sweep -- 1M ctx, $10/$50 with a $0.025x cache-hit rate; NOT yet in Cyberine's catalog/pricing, see the T708 sweep note below). Aliases: sonnet/opus/haiku/best/opusplan/sonnet[1m]/opus[1m]; fable and best resolve to Fable 5 on gateways not yet configured (CC v2.1.257).

stream-json event schema

  • Final result event (what Cyberine parses — parseClaudeStreamJson): {type:'result', subtype:'success', result:<string>, session_id, duration_ms, num_turns, total_cost_usd, usage:{input_tokens, output_tokens}, modelUsage:{...}, terminal_reason}.
  • Error subtypes: error_max_turns / error_during_execution / error_max_budget_usd / error_max_structured_output_retries (no result; errors[] instead). v2.1.272 added turn_preempted. Since kq5rd (2026-09-16) parseClaudeStreamJson fails the run on ANY present subtype other than success, naming it, so a new subtype cannot land as an empty success.
  • Intermediate (need --verbose): system/init (first; session/model/tools/plugins), assistant, tool_use, tool_result, system/api_retry.
  • Token deltas (need --include-partial-messages): nested .event.delta.type=='text_delta'.
  • Session topology (T333, parseClaudeSessionTree): system/init events carry session_id + optional parent_session_id; a session with no parent is the root.
  • rate_limit_event (kxvc9, live-probed 2026-09-06 on v2.1.263) — its OWN top-level line, one per turn, immediately before the final result, present on an ordinary -p call and not only when limited. Probe: claude -p "Reply with exactly: ok" --model haiku --output-format stream-json --verbose --permission-prompts none | grep rate_limit_event. Shape: {type:'rate_limit_event', rate_limit_info:{status:'allowed'|'allowed_warning'|'rejected', resetsAt:<epoch SECONDS>, rateLimitType:'five_hour'|'seven_day'|..., unifiedWindows:{five_hour:{utilization,resetsAt}, seven_day:{utilization,resetsAt}}, overageStatus, overageDisabledReason, isUsingOverage}, uuid, session_id}. utilization is a 0..1 fraction per window; the 5-hour and 7-day windows are SEPARATE numbers. There is no headless "remaining usage" endpoint (/usage is interactive-only, no claude usage subcommand, verified on v2.1.261-263): this event is the only readout, and parseClaudeRateLimitEvent (src/adapters/claudeStreamJson.ts) reads the LAST one per turn, normalising a millisecond resetsAt to seconds. UNVERIFIED (kp5x7, 2026-09-06): whether a window that runs out MID-turn ends the turn with is_error: true and a rejected event, or only on the next turn. Cyberine's autopilot classifies both shapes (event status, or result text matching the rate-limit vocabulary) so either behaviour pauses the loop; record the observed shape here the first time a real run hits it.

Gotchas

  • stdin-closed subprocesses stall forever on the first permission prompt without --dangerously-skip-permissions (no timeout fallback).
  • --add-dir grants file access only — .claude/ config is NOT loaded from those dirs.
  • --bare ignores CLAUDE_CODE_OAUTH_TOKEN.
  • ANTHROPIC_API_KEY takes precedence over subscription OAuth; unset it to fall back.
  • --verbose required for intermediate events; Cyberine parses the last type:'result' line.
  • Cache token sub-fields may appear in modelUsage entries but aren't documented top-level usage fields — don't assume. CORRECTED 2026-09-06 (keep5), live-probed on the installed v2.1.261. result.usage DOES carry cache_read_input_tokens and cache_creation_input_tokens at the top level, alongside input_tokens / output_tokens, plus output_tokens_details, cache_creation (ephemeral_5m_input_tokens / ephemeral_1h_input_tokens), iterations[], service_tier and server_tool_use. Captured from claude -p "Reply with exactly: probe-ok" --output-format stream-json --verbose --permission-prompts none --model haiku. This matters because the SIZE OF THE CONTEXT is input_tokens + cache_read_input_tokens + cache_creation_input_tokens — on a warm resumed session input_tokens is a literal handful (measured: 2, against a real context of 67,410) while the rest arrives as cache_read_input_tokens. Anything reading input_tokens alone is not approximating the context, it is reporting a different quantity. src/adapters/claudeStreamJson.ts (readClaudeTurnTokens) is the one reader; it still defaults every field to 0, so a binary that omits them degrades to a smaller number rather than a crash. CORRECTED AGAIN 2026-09-06 (run 0dec102d), live-probed on v2.1.261 with a two-call turn. result.usage is the turn TOTAL: the CLI sums every model call it made (one per tool round). Probe (claude -p "Run echo ... then reply probe-ok" --allowedTools Bash --model haiku): call 1 usage {input 10, cache_read 12166, cache_creation 22343}, call 2 {8, 34509, 259}, and result.usage = {18, 46675, 22602} — the field-wise sum, 69,295 "context" for a session whose real context was 34,776. On a 114-call turn the same sum read 9,864,289 tokens for a 195,653-token session and the autopilot's fill policy saw 986%. The context of a turn is the prompt of its LAST call, and each top-level assistant event carries that call's own message.usage (one event per content block, identical usage, same message.id; subagent events carry parent_tool_use_id and their own context). readClaudeContextTokens(stdout, resultEvent) is the reader for that; the readClaudeTurnTokens totals stay correct as TOTALS (cost attribution, per-turn accounting).
  • result.modelUsage is keyed by the RESOLVED model id (probed: claude-haiku-4-5-20251001), which is how a caller learns which model actually answered — a bare claude -p with no --model inherits whatever /model the operator last picked interactively.
  • From 2026-06-15, claude -p / Agent SDK usage on subscription plans draws a separate monthly Agent SDK credit.
  • Background Bash tasks are killed ~5s after the final result (v2.1.163).
  • kvks8, live-probed 2026-09-18 on v2.1.276 — do not re-probe blind. The Monitor tool's 10-minute hard deadline in -p mode (v2.1.271+) does NOT stall a stdin-closed headless worker: it fires a task_notification {"status":"stopped"} with no user input required, the model re-arms Monitor unprompted in its very next turn, and the real background task's own completion arrives via a SEPARATE, always-on task_notification {"status":"completed"} channel that is not gated on Monitor being currently armed — so even a re-armed Monitor that never itself detects an already-finished task just idles out its own new deadline as one harmless extra turn, it never blocks the real result. Measured: a worker told to background a 12-minute command and Monitor-wait for it reached result/exit 0 at 15.3 min wall time (12.1 min real work + ~3.2 min overhead from the deadline-then-redundant-expiry pattern), comfortably inside the adapter's default 30-minute timeoutMs (config.openCodeTimeoutMs/CYBERINE_CLI_TIMEOUT_MS). No adapter change made — see .local/research/2026-09-18-kvks8-monitor-deadline.m3max.md for the full event timeline.
  • --safe-mode isolates config/customizations but not auth, model selection, or permissions — the explicit --dangerously-skip-permissions and --system-prompt overrides still apply on top of it.
  • v2.1.212+: SIGTERM in print/SDK mode now exits 143 cleanly (no orphaned process tree) — CliAgentAdapter never inspects the exit code on its own SIGTERM-driven cancel path (it short-circuits on an internal aborted flag first), so this needed no adapter change (T646).
  • v2.1.212+: new per-session caps CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION / CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION (default 200 each) can silently truncate a research-heavy or internally-fan-out-heavy single claude session. Override via CYBERINE_CLAUDE_MAX_WEB_SEARCHES_PER_SESSION / CYBERINE_CLAUDE_MAX_SUBAGENTS_PER_SESSION (T646) — absent (default) leaves CC's own 200 in force.
  • T627, GO decision, recorded 2026-08-27 — do not re-probe blind. Installed CLI: 2.1.246 (Claude Code). Probe 1: claude -p --output-format json --no-session-persistence "reply with exactly: probe-ok" — the final line still carried "session_id":"a9e917d2-..." (plus a full result payload); no file matching that session id was ever written under ~/.claude/projects (checked via find), confirming the flag suppresses only the on-disk transcript, not the reported id. Probe 2 (cheap model, the real shape this adapter uses): claude -p --model haiku --output-format stream-json --verbose --no-session-persistence "reply with exactly: probe-ok" — the system/init event still carried "session_id":"22207914-..." (no parent_session_id, correctly a root session) and the final result event repeated the same session_id; again no transcript file was written for that id. Those two fields are exactly what parseClaudeSessionTree (T333) reads, so session-topology capture is unaffected by the flag in principle — makeClaudeAdapter still omits the flag whenever opts.captureSessionTree is set, kept conservative on purpose (see the inline comment at the flag's call site in src/adapters/CliAgentAdapter.ts). Decision: adopted on the cold-spawn path, scoped !opts.warmSession && !opts.captureSessionTree; regression guard npm run selftest:no-session-persistence (scripts/t627-no-session-persistence-smoke.ts).
  • T695, three CONFIRMED-HARMLESS decisions, recorded 2026-08-29 — do not re-probe blind. Filed by the 2026-08-28 R02 sweep against CHANGELOG-WATCH's reviewed high-water mark v2.1.247; live-checked against the installed v2.1.251 binary (claude --version) and the raw anthropics/claude-code CHANGELOG.md (WebFetch, 2026-08-29). Version correction: the R02 sweep's note attributed the SIGTERM delta to v2.1.247 and the MCP headersHelper deltas to v2.1.238+v2.1.239 — the raw changelog places the SIGTERM bullet at v2.1.234 (verbatim match otherwise) and all four headersHelper/trust-dialog bullets at v2.1.238 alone (no v2.1.239 hit); the sub-agent model-404 fix is correctly v2.1.247. Immaterial to the verdicts below since the installed binary (v2.1.251) is past all of them either way.
    1. SIGTERM in print/SDK mode no longer records an interrupted turn or synthetic tool denials before exiting (v2.1.234). Harmless: parseClaudeStreamJson only ever reads the final type:'result' stream-json line and never branches on subtype or looks for interrupted/denial markers, so it never depended on the removed records; the CliAgentAdapter timeout/cancel classification (aborted/timedOut flags) never inspects transcript content either (T646 already established this for the exit-code side). Doubly moot for the on-disk transcript reading of the same changelog line (relevant to a future resume): runStreaming's session cache never stores a session ID for warm reuse when result.status === 'error' || result.status === 'cancelled' (see the two guards around lines ~712 and ~738 in src/adapters/CliAgentAdapter.ts), so a claude worker killed by timeout or by cancel is never subsequently resumed by Cyberine — whatever that session's persisted transcript does or doesn't record about the interrupted turn is never read back. No code change; no new smoke.
    2. MCP headersHelper in a project .mcp.json, and inline MCP servers in project or --add-dir agent files, now require that folder's trust dialog to have been accepted (also under claude -p) (v2.1.238). Harmless, doubly so: (a) the T488 MCP-bridge gap is still open — no claude build worker is ever spawned with --mcp-config, so Cyberine never wires an MCP server into a worker's argv in the first place; (b) makeClaudeAdapter applies --safe-mode unconditionally on every build-worker spawn (cold or resumed), and claude --help describes it as disabling "all customizations (CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands and agents, ...)" — so even this repo's own committed root .mcp.json (a plain cyberine-mcp command entry, no headersHelper) is inert for a spawned worker regardless of the trust-dialog change. No code change; no new smoke.
    3. Fixed sub-agents dying on a first-call model 404: they now use the session's fallback model chain (v2.1.247). Harmless: this fix is scoped to Claude Code's own internal Task-tool sub-agents inside a session, not the top-level process CliAgentAdapter spawns or its --model/--fallback-model selection. More importantly, Cyberine never reads which model actually served a request from the CLI's own output — Result.provider (claude:${model}) is set once, statically, from the configured model at adapter construction (makeClaudeAdapter, src/adapters/CliAgentAdapter.ts) and propagated unchanged into WorkerRun.provider via agent.info.provider (src/orchestrator/orchestrateAsync.ts); parseClaudeStreamJson extracts only result/usage/total_cost_usd/opt-in extras, never a model id, from the stream-json events. So a silent internal fallback (sub-agent-scoped, or even a top-level one under an opted-in --fallback-model chain — already-existing, pre-dating this fix) has no field to corrupt: the recorded provider/model was always "what was requested," never "what actually served," before or after this change. No code change; no new smoke.

2026-09-04 sweep (T708): v2.1.248 -> v2.1.260, each version dispositioned

The row's baseline said v2.1.247 while the installed binary reported v2.1.260 -- wider than the gap T708 was filed for (it named 2.1.248-2.1.251, measured when the binary was 2.1.251). Diffed the raw anthropics/claude-code CHANGELOG.md for all 13 releases against the surface Cyberine actually drives (claude -p, --output-format stream-json --verbose, --safe-mode, --dangerously-skip-permissions, --no-session-persistence, --fallback-model, --model, --forward-subagent-text, --exclude-dynamic-system-prompt-sections, session resume, SIGTERM). 2.1.253-2.1.256 have no changelog entries; 2.1.250 is "Bug fixes and reliability improvements".

Both filed tasks are now CLOSED — see the two verdict blocks below (k7hha) and the catalog row (kmq47, claude-fable-5-1 added to modelCatalog.ts + modelPricing.ts).

k7hha verdicts: --permission-prompts none ADOPTED, --restricted DECLINED

--permission-prompts <host|none> (v2.1.259) — ADOPTED, unconditional on every claude spawn. Verbatim from claude --help on the installed v2.1.260: "Who answers permission prompts with --print: 'host' (the SDK host or --permission-prompt-tool) or 'none' (nobody: anything that would prompt is denied automatically; the permission mode still decides everything else) (choices: 'host', 'none', default: 'host')". The DEFAULT is the defect for this adapter: a worker is spawned with stdin closed and with neither an SDK host nor --permission-prompt-tool, so under host a prompt goes to an answerer that does not exist and can only stall to the adapter's own timeout with no exit code. --dangerously-skip-permissions covers only what the permission MODE decides — a folder trust dialog (T695 found the v2.1.238 MCP headersHelper change can raise one under claude -p, in a throwaway worktree no operator has ever trusted) or an MCP elicitation is not that. Deny-and-fail-fast beats a 30-minute hang. Live-probed before adoption (T627's precedent), v2.1.260, --output-format json alongside --dangerously-skip-permissions --safe-mode: a plain turn returned is_error: false, subtype: "success", result: "OK", and a TOOL-USING turn (write a file) returned is_error: false, permission_denials: [], num_turns: 2 with the file actually written — so the flag does not turn skip-permissions' auto-allow into an auto-deny. No disable knob on purpose: an operator who wants the old behaviour wants a hang. Registered in CLI_FLAG_COMPAT (knownGoodVersion: 2.1.259) so an older binary gets the named T656 diagnosis. Guard: npm run selftest:permission-prompts.

--restricted (v2.1.248) — DECLINED, no posture to apply it to. Verbatim: "Restricted mode: removes the built-in tools that run commands or code (Bash, PowerShell, REPL and the other code-running tools) and WebFetch unless --tools names them, and ignores user, project and local settings files (managed settings and --settings still ...)". Wrong for a BUILD worker by construction — it removes exactly the tools the worker exists to use — and there is no read-only claude posture left to give it to: makeClaudeCodeAdapter, the --bare read-only variant, was deleted as dead code by T622 (nothing called it and buildAdapters.ts had no case 'claude-code':). Its settings-ignoring half is already covered for every spawn by --safe-mode. Re-evaluate only if a read-only claude worker is reintroduced; adopting it now would mean inventing the caller first.

MATERIAL, task filed:

  • v2.1.257: new model claude-fable-5-1. Verbatim: "Added Claude Fable 5.1 (claude-fable-5-1), 1M context, $10/$50 per Mtok with $0.25/Mtok cache". Cyberine's catalog and pricing carry only claude-fable-5 (modelCatalog.ts, modelPricing.ts) -- the same gap class k3kbw closed for claude-opus-5. Cross-checked against the live pricing page rather than trusting the release note (the k3kbw lesson: that note's $10/$50 for opus-5 was the FAST-MODE rate, not standard). This time the note is right, and the page adds a detail the note compresses: base input $10 / MTok, output $50 / MTok -- identical to Fable 5 -- but the CACHE-HIT rate is $0.25 / MTok, because "Cache hits and refreshes on Claude Fable 5.1 and Claude Mythos 5.1 are priced at 0.025x the base input price. All other models use the standard 0.1x multiplier." Fable 5 charges $1/MTok for the same read. 5m cache write $12.50, 1h $20, batch $5/$25. Filed.
  • v2.1.248 --restricted and v2.1.259 --permission-prompts none, both new flags on the headless path: "Added --restricted removing built-in command/code tools and WebFetch" and "Added --permission-prompts none for unattended headless hosts to deny anything prompting". Neither is adoptable for a BUILD worker (which needs exactly the tools --restricted removes, and already runs --dangerously-skip-permissions), but both are candidate postures for a read-only/research worker and for the belt-and-braces case where a permission prompt would otherwise stall a stdin-closed spawn. Filed as one evaluation task, not implemented inline.

HARMLESS, recorded with evidence, no code change:

  • v2.1.251: "Changed default commit trailer to Co-Authored-By: Claude Code". This repo's global git rule bans that trailer outright, so the change is worth naming -- but it cannot reach a Cyberine commit: every commit Cyberine makes is made by Cyberine's own code (swarmLaneRunner.ts's git commit -q, kanbanRunner.ts's auto-commit) with an explicit message, and captureDiff reads git diff --cached after its own git add -A, so the winner path never depends on a worker committing. The residual exposure is a claude worker that decides to commit on its own mid-task; the trailer would then land in that worktree commit, which is discarded rather than merged (the diff is what ships). Recorded, not guarded.
  • v2.1.251: "Changed CLAUDE_CODE_SUBAGENT_MODEL setting default over agent definition", plus v2.1.257's "Added CLAUDE_CODE_SUBAGENT_MODEL_FORCE to apply model to every subagent". Cyberine never sets either var for a worker (they are absent from makeClaudeAdapter's envSet and from CliAgentAdapter's team-var re-add list), but a trusted spawn inherits the operator's environment -- so an operator who exports CLAUDE_CODE_SUBAGENT_MODEL=opus now has that win over a worker's own agent definitions where the definition used to win. A cost note, not a correctness one: it changes which model a claude worker's INTERNAL subagents bill to, and Cyberine reads cost from the CLI's own total_cost_usd, so the number stays truthful either way.
  • v2.1.257 + v2.1.259: three worktree/sandbox FIXES land on the side Cyberine is on. Verbatim: "Fixed worktree-isolated sessions refusing complex Bash, $VAR reads, heredocs", "Fixed worktree-isolated sessions refusing common Bash loops and xargs pipelines", "Fixed sandboxed git in linked worktree losing write access after cd to subdirectory". Every isolate step spawns its worker inside a detached git worktree (worktreeSandbox.ts) under the host sandbox, so these are exactly the shapes a build worker runs -- the installed 2.1.260 is the fixed side, and no Cyberine change is needed to benefit.
  • v2.1.260: "Fixed -p --resume/--continue failing on retry when worktree directory lost git metadata". Same class, same direction: the warm-session resume path (opts.warmSession) drives -p --resume inside a worktree.
  • v2.1.258: "Fixed Claude Code failing to launch on macOS 12 (Monterey), regression from 2.1.255". Informational -- this machine is on Darwin 25, but it names a two-release window in which a claude worker would not have started at all on an older macOS.
  • v2.1.260's Reverted 2.1.259 change applying Read() deny rules to Bash arguments, and every other entry across the 13 releases: interactive TUI, VSCode, Remote Control, Desktop, gateway, managed-settings, plugin, /slash-command and agent-team surfaces. None touches the print-mode spawn, the stream-json shape parseClaudeStreamJson reads, or a flag Cyberine passes.

kz74p: --bg refuses an untrusted cwd when not interactive (CC 2.1.281+) — READ FROM THE 2.1.282 BINARY 2026-09-25

Changelog 2.1.281: "Fixed claude --bg starting a background session, and running its project hooks, in a directory that had not passed the workspace trust prompt; it now asks for trust first, or exits when not run interactively". Read from the installed 2.1.282 binary's bundled source (not a live spawn, which would bill a turn and register a fleet row if the check passed):

  • The --bg dispatch runs a trust check on the cwd before anything else. Its verdicts: sandboxed when CLAUDE_CODE_SANDBOXED is set, persisted when the cwd has accepted trust or sits under a root that has (the code consults the git root and the launching cwd as candidate roots; the exact walk was not traced, so a Cyberine worktree under <repo>/.claude/worktrees/ inheriting the repo's trust is likely but unconfirmed), session for the home directory on an interactive terminal, else untrusted.
  • When stdin and stdout are both a TTY it shows the trust prompt. Otherwise an untrusted verdict writes to stderr and sets exit code 1, with no job id on stdout: Workspace not trusted. Run `claude` in <dir> once and accept the trust prompt, then retry. (an unresolvable path gets Workspace not trusted. <dir> could not be resolved on disk.; the home directory gets a home-specific line). --dangerously-skip-permissions and --permission-prompts none do not bypass it.
  • claude respawn on an untrusted job fails the same way with errorCode: workspace_untrusted.

Cyberine's side: createBgSessionDriver (every claude --bg spawn, including ClaudeBgAdapter) maps a non-zero exit whose stderr matches Workspace not trusted. to BgSessionDriverError code untrusted-cwd, naming the cwd and the fix, instead of the generic spawn-failed. It does not pre-trust a directory: writing trust into ~/.claude.json on the operator's behalf would defeat the point of the prompt. Pinned by the kz74p case in npm run selftest:autopilot-bg-driver (fake CLI printing the real line).

k56eq: a --bg session job is NOT a child of the client — MEASURED 2026-09-08, do not re-probe blind

Installed CLI 2.1.263. A claude "<prompt>" --bg --name cyberine-w-probe1-s1 --model haiku --permission-prompts none --dangerously-skip-permissions client was spawned from a Node process whose environment had CYBERINE_BG_PROBE_MARKER=present ADDED and SHELL DELETED. The job wrote CYBERINE_BG_PROBE_MARKER=unset, SHELL=SET, HOME=SET, CYBERINE_TEAM=unset (presence tests only, no values). Both halves are load-bearing: the added marker never arrived, and the deleted SHELL still did. The worker runs under the daemon with the daemon's own recorded environment; nothing Cyberine puts in the client's spawn env reaches it, and nothing it removes is removed. The client exited 0 while the job ran on. This extends kwx2r's --exec measurement to the --bg SESSION form. Three consequences, all now enforced in src/adapters/ClaudeBgAdapter.ts:

  • hostSandbox.ts cannot confine a bg worker. wrapSpawn would wrap the short-lived client only. The bg adapter never wraps, reports Result.raw.sandbox: 'none', and REFUSES with HostSandboxError when CYBERINE_TASK_TRUSTED=false.
  • envScrub cannot be applied, so it is enforced by refusal: a metered key present in Cyberine's own env blocks the spawn (the error names the key, never the value) unless CYBERINE_CLAUDE_BG_ALLOW_METERED_ENV=1.
  • --settings '{"env":{...}}' DOES deliver environment to a daemon-spawned job. Live-verified with a control on the same CLI: a job spawned with --settings '{"env":{"CLAUDE_CODE_DISABLE_BUNDLED_SKILLS":"1","CLAUDE_CODE_RETRY_WATCHDOG":"1"}}' reported both as SET, while CYBERINE_K56EQ_CONTROL (in neither --settings nor the client env) reported unset. Only SETTING is proven; whether an empty value can NEUTRALISE an inherited variable was NOT measured, which is why the refusal above is not replaced by an empty-string trick. A second --settings REPLACES the first, so the env block and kqq2g's bashOutputMaxChars share one JSON object.

The same job's transcript (~/.claude/projects/<realpath cwd slug>/<sessionId>.jsonl) carried the last top-level assistant message.usage ({input_tokens, cache_read_input_tokens, cache_creation_input_tokens, output_tokens} plus output_tokens_details / cache_creation / iterations / service_tier), one system/turn_duration, and neither total_cost_usd nor rate_limit_event anywhere in the file — so a bg worker has no cost and no live quota signal, by construction rather than by assumption. Guard: npm run selftest:bg-fleet-worker.

Background-job (claude agents) state vocabulary — MEASURED 2026-09-08 on 2.1.263

claude agents --json --all rows carry {id, cwd, kind, name, sessionId, startedAt, state} — no usage, no exit code. state is one of starting | working | blocked | idle | done | stopped | failed (the seven literals present in the 2.1.263 binary; a live fleet of 37 showed done 20, blocked 16, working 1, and null for the 3 kind: interactive rows, which have no daemon state). The row's private ~/.claude/jobs/<id>/state.json (read-only for us) adds tempo (active | blocked | idle), needs (free text: the prompt the session is waiting for, e.g. send your next prompt to the successor agent), detail (last output line), suggestedReply, inFlight, template (bg | exec), respawnFlags, firstTerminalAt / lastTerminalAt. The TUI label is DERIVED: blocked + a needs text renders Needs input; blocked with needs: null is a turn that ended and awaits the operator (the same row rendered Failed for a minute right after claude respawn --all replaced its process); done renders Done; failed renders Failed with the exit code in detail (an exec row killed by SIGTERM lands here as exit 143); stopped after claude stop; starting between dispatch and the first output. A blocked row is not an error — for a Claude session it is the normal between-turns state. An exec row (template: exec, pid: null) is never blocked / Needs input: the daemon derives its working / idle from PTY output activity (~30 s of silence -> idle). Cyberine readers: src/autopilot/bgSessionDriver.ts (BG_IDLE_STATES, turn-boundary detection), src/adapters/ClaudeBgAdapter.ts.

Pricing

Subscription (Pro/Max/Team/Enterprise) or API pay-as-you-go. From 2026-06-15, headless usage on subscriptions draws a separate Agent SDK credit. https://claude.com/pricing

Cyberine integration notes

  • makeClaudeAdapter (build worker claude) gated on install + headless login (cliAvailability.ts); spawned under the host sandbox with ~/.claude writable.
  • Flag-compat diagnosis (T656): an older installed claude that rejects a version-gated flag (commander emits error: unknown option '--flag', exit 1) surfaces a named error carrying the flag, the owning task, a known-good version, and the disable knob — registry CLI_FLAG_COMPAT in src/adapters/CliAgentAdapter.ts, smoke npm run selftest:cli-flag-compat.
  • Underlying API: anthropic.md.

Sources