Headless streaming and resilience
This page documents the non-interactive execution path used by claude -p, SDK transports, --init-only, and non-TTY stdout.
Source anchors
| Semantic alias | Source | Approximate location | String or symbol | Meaning |
|---|---|---|---|---|
| HeadlessModePredicate | cli.js | line ~19524, byte 0xdbe925 | `$.includes(“-p”) | |
| HeadlessMcpCoordinator | cli.js | line ~19543, byte 0xdc73fa | let o4=fH9({regularMcpConfigs:Ww | Headless branch creates MCP coordinator. |
| HeadlessRunnerLazyImport | cli.js | line ~19543, byte 0xdc75cf | let{runHeadless:u7}=await Promise.resolve().then(() => (M89(),O89)) | Lazy imports headless runner. |
| HeadlessRunner | cli.js | line ~19324, byte 0xda31bb | async function T7A | runHeadless implementation. |
| HeadlessControlLoop | cli.js | line ~19349, byte 0xda50d0 | function H89 | Main headless streaming/control loop. |
| OutputFormatFlag | cli.js | line ~19525, byte 0xdbfdf6 | --output-format <format> | text, json, and stream-json output selector. |
| InputFormatFlag | cli.js | line ~19525, byte 0xdc0152 | --input-format <format> | text or stream-json input selector. |
| SdkUrlTransportFlag | cli.js | line ~19550, byte 0xdcb520 | --sdk-url <url> | Remote WebSocket endpoint for SDK I/O streaming. |
| SdkPermissionControlFrame | cli.js | line ~2004, byte 0x519ca7 | can_use_tool control_request | Permission prompt/control frame surface for SDK hosts. |
| BridgePermissionResponseFrame | cli.js | line ~64, byte 0x39a71 | permission_response | Remote/bridge permission response frame. |
Headless flow
flowchart TD Args[-p / --print / --sdk-url / non-TTY] --> Validate[Validate input and output formats] Validate --> Prompt[Read prompt and stdin] Prompt --> Setup[Load settings, tools, agents, MCP] Setup --> MCP[MCP runtime coordinator] MCP --> Runner[Headless runner] Runner --> Loop[Headless streaming/control loop] Loop --> Result[text / json / stream-json result]Format and control surfaces
| Surface | Runtime role |
|---|---|
| `—output-format text | json |
| `—input-format text | stream-json` |
--sdk-url <url> | Requires stream-JSON input and output and connects to a remote SDK endpoint. |
--include-partial-messages | Emits partial message chunks for stream-JSON print mode. |
--replay-user-messages | Re-emits user messages from stdin for stream-JSON acknowledgement. |
--json-schema <schema> | Adds structured-output validation for print mode. |
control_request | Host-facing request frame family. |
can_use_tool | Permission prompt request subtype. |
permission_response | Host/bridge response to a permission prompt. |
mcp_tool_call | MCP tool-call telemetry/error surface in the headless/runtime path. |
Resilience and guardrails
The headless runner validates several incompatible combinations before executing:
--resume-session-atrequires--resume.--rewind-filesrequires--resumeand cannot be used with a prompt.- SDK URL mode requires stream-JSON input and output.
- Partial messages require print mode and stream-JSON output.
- Print mode requires input unless the resume/SDK path supplies it.
HeadlessControlLoop is the headless equivalent of the interactive dispatcher. It handles stream input, permission/control requests, MCP status and calls, background-task control, bash command messages, session state, and result emission.
Control-loop internals
This section deepens the surface above by reconstructing the implementation mechanics of HeadlessRunner and HeadlessControlLoop.
Additional anchors
| Semantic alias | Source | Approximate location | String or symbol | Meaning |
|---|---|---|---|---|
| ResumeSessionAtGuard | cli.js | line ~19324, byte 0xda33e5 | Error: --resume-session-at requires --resume | Resume truncation guard. |
| RewindFilesResumeGuard | cli.js | line ~19325, byte 0xda3455 | Error: --rewind-files requires --resume | Rewind guard. |
| RewindFilesStandaloneGuard | cli.js | line ~19326, byte 0xda34b8 | Error: --rewind-files is a standalone operation and cannot be used with a prompt | Rewind is standalone, not prompt-plus-rewind. |
| SdkStartupPhaseLogger | cli.js | line ~19324, byte 0xda34f0 | SDKStartup: phase= | SDK startup phase logging for remote transport setup. |
| HeadlessResultErrorTypes | cli.js | line ~2004, byte 0x5167df | error_max_turns, error_max_budget_usd, error_max_structured_output_retries | Result error subtypes in the headless schema. |
| HeadlessHookFrames | cli.js | line ~2004, byte 0x518233 | hook_started, hook_progress, hook_response | Headless hook lifecycle system frames. |
| HeadlessOutboundChannel | cli.js | line ~19349, byte 0xda50d0 | let h=H.outbound | H89 uses an outbound queue/channel abstraction. |
| RateLimitEventFrame | cli.js | line ~19349, byte 0xda5683 | rate_limit_event | Rate-limit state changes are emitted into the outbound stream. |
| McpElicitationCompleteFrame | cli.js | line ~19349, byte 0xda6206 | elicitation_complete | MCP elicitation completion is bridged into system frames. |
| BridgeStateFrame | cli.js | line ~19356, byte 0xdaf5aa | bridge_state | Remote/SDK bridge state transitions are emitted as system frames. |
Runner setup
HeadlessRunner is entered after the root action has validated the high-level print/SDK mode and built a state object, tool lists, MCP configs, active agents, output options, and session hooks. Mechanically, the beginning of HeadlessRunner does four things before it reaches the model loop:
- Subscribes to settings/state changes. The function starts with a
TI.subscribe(...)hook that can update headless state, including fast-mode state. - Enables periodic garbage collection.
setInterval(Bun.gc,1000).unref()is explicit in the function body — a Bun-specific runtime detail. - Records startup telemetry.
tengu_timeris emitted with startup duration, MCP server count, and whether the run is resumed. - Runs hard validation before model execution — rejects invalid resume/rewind combinations before any main loop work.
| Guard | Condition | Effect |
|---|---|---|
| Resume truncation | resumeSessionAt without resume | Writes Error: --resume-session-at requires --resume and exits. |
| Rewind without resume | rewindFiles without resume | Writes Error: --rewind-files requires --resume and exits. |
| Rewind with prompt | rewindFiles plus prompt text | Writes Error: --rewind-files is a standalone operation and cannot be used with a prompt and exits. |
Rewind is therefore implemented as a standalone transcript/file-state operation, not as an extra option on a normal prompt run.
SDK startup and transport boundary
HeadlessRunner computes a bridge/SDK condition from sdkUrl and CLAUDE_CODE_ENVIRONMENT_KIND. When SDK transport logging is enabled, it writes phase markers such as SDKStartup: phase=<phase> t=<seconds>s. These markers show that SDK-mode startup is a staged transport initialization path, not only a different stdout format. CLAUDE_CODE_SDK_HAS_OAUTH_REFRESH and CLAUDE_CODE_ENTRYPOINT are also checked here, so OAuth refresh and SDK entrypoint classification participate before the control loop begins.
Headless outbound stream model
HeadlessControlLoop starts by binding an outbound stream/channel. The outbound stream is not just model text — it multiplexes system state, auth, MCP, plugin, bridge, prompt-suggestion, task, and final result frames:
| Frame type or subtype | Meaning |
|---|---|
transcript_mirror | Internal frame emitted after transcript writes when session mirroring is enabled. |
auth_status | Authentication progress/status frame. |
rate_limit_event | Rate-limit changes are streamed to SDK/headless consumers. |
elicitation_complete | MCP URL-mode elicitation completion is surfaced. |
plugin_install | Synchronous plugin-install progress can be streamed. |
task_notification | Background task/monitor status is streamed. |
prompt_suggestion | Predicted next prompt can be emitted after a turn. |
bridge_state | Remote/SDK bridge state changes are surfaced. |
control_response | Responses to inbound control requests. |
result | Final run result, including success or error subtype. |
Control-loop side channels
flowchart TD Runner[Headless runner setup] --> Loop[Headless control loop] Loop --> Outbound[Outbound stream frames] Loop --> MCP[MCP clients and elicitation] Loop --> Bridge[Remote/SDK bridge] Loop --> Plugins[Sync plugin install] Loop --> Tasks[task_notification frames] Loop --> Suggestions[prompt_suggestion frames] Loop --> Result[final result]
MCP --> Outbound Bridge --> Outbound Plugins --> Outbound Tasks --> Outbound Suggestions --> OutboundImportant mechanics inside HeadlessControlLoop:
- Enables
transcript_mirrorframes when stream JSON plus session mirror is active. - Subscribes to auth status and rate-limit changes and converts them to outbound frames.
- Watches MCP client changes and registers elicitation completion handlers.
- Emits
bridge_stateframes when the Remote Control/SDK bridge changes state. - Supports synchronous plugin installation frames behind
CLAUDE_CODE_SYNC_PLUGIN_INSTALL. - Can start a cron scheduler when recurring task support is enabled; scheduled prompts are enqueued later into the loop.
Result and error model
The result schema around line ~2004 differentiates normal results from structured error results. The error subtype enum includes error_during_execution, error_max_turns, error_max_budget_usd, and error_max_structured_output_retries, so headless callers can distinguish execution error, turn limit, budget limit, and structured-output retry exhaustion.
Caveats
HeadlessControlLoopis large and minified. This section documents confirmed side channels and frame families, not every branch.- Some frame schemas are defined outside
HeadlessControlLoopnear line ~2004 and are included here only when the loop also emits or references the same frame family.
Related docs
Created and maintained by Yingting Huang.