Files
byre/docs/claude-protocol.md
T

57 lines
5.8 KiB
Markdown
Raw Normal View History

# Claude Code stream-json protocol
How `internal/claude` drives the `claude` CLI. Worked out from the Agent SDKs (`@anthropic-ai/claude-agent-sdk` on npm, `claude-agent-sdk` on PyPI); the Python SDK's `_internal/query.py` and `_internal/transport/subprocess_cli.py` are the most readable references. Each test run writes its transcripts to `internal/claude/testdata/transcripts/` (gitignored); the CLI version that run used is `claude_code_version` in the `system`/`init` event of any of them.
Transcripts (`Options.Transcript`) record every line in both directions as `{"dir":"send"|"recv","msg":...}`. The account email from the `initialize` response is replaced with `[redacted]` everywhere it appears.
## Launch
```
claude --output-format stream-json --input-format stream-json --verbose \
2026-10-14 19:35:34 -07:00
--include-partial-messages \
--permission-mode bypassPermissions --allow-dangerously-skip-permissions \
--settings '{"attribution":{"commit":"","pr":""}}' \
--mcp-config '{"mcpServers":{"byre":{"type":"sdk","name":"byre"}}}' \
[--model M] [--resume=SESSION_ID] [--fork-session]
```
- No `--print`; stream-json input implies non-interactive.
- `--resume=` uses the equals form so a dash-leading value can't be parsed as a flag.
- Environment: `CLAUDECODE` is removed so the child doesn't think it is nested; `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set the commit identity; `attribution` set to empty strings removes the co-author trailer.
## Framing
NDJSON both ways: one JSON object per line on stdin and stdout. Non-JSON stdout lines are ignored. Stdin EOF ends the process.
## Messages
2026-10-14 20:07:26 -07:00
- User turn, written to stdin: `{"type":"user","message":{"role":"user","content":"..."},"parent_tool_use_id":null,"session_id":"..."}`. With images, `content` is an array of Anthropic content blocks instead: `{"type":"image","source":{"type":"base64","media_type":"image/png","data":"..."}}` for each image, then `{"type":"text","text":"..."}`.
- Stdout events: `system` (subtype `init` carries `session_id`, `model`, tools, MCP server status), `assistant` and `user` (Anthropic message content blocks: `text`, `tool_use`, `tool_result`), `result` (ends a turn; `subtype` is `success` or `error_during_execution` after an interrupt, with cost, duration, turn count).
2026-10-14 19:35:34 -07:00
- `--include-partial-messages` adds `stream_event` lines wrapping the raw API stream (`message_start`, `content_block_start`, `content_block_delta` with `text_delta`, `content_block_stop`, `message_delta`, `message_stop`), each with `parent_tool_use_id` (set inside subagents). The complete `assistant` event for a block still arrives, before that block's `content_block_stop`. Transcripts leave `stream_event` lines out.
- The `init` event arrives with the first turn, not at launch, so the session ID is unknown until the first message is sent.
- While a tool runs, `tool_progress` lines with `"heartbeat": true`, the tool's `elapsed_time_seconds`, and a `parent_tool_use_id` naming the tool call arrive every 30 seconds. A Bash call also brings `system` `task_started` and `task_notification` lines around it.
- Subagents (the `Agent` tool, listed in `init` tools as `Task`): `system` `task_started` (`task_type` `local_agent`, `tool_use_id` naming the `Agent` call, `subagent_type`, `description`, `prompt`, `is_backgrounded`), `task_progress` (`description` of what it is doing), `task_updated` (by `task_id` only), and `task_notification` (`status`, `summary`). `assistant` and `user` lines from inside the subagent carry `parent_tool_use_id` set to the `Agent` call's ID, plus `agent_id`, `subagent_type` and `task_description`. A foreground subagent (`run_in_background: false`) sends only its prompt this way, as a `user` text block, and its final report arrives as the `Agent` call's `tool_result`. A background one (the default) streams all its assistant and tool-result lines; the `Agent` call's `tool_result` comes back at once saying it launched, the turn can end with its own `result` while the subagent runs, and when the subagent finishes the CLI starts another turn by itself, ending in a second `result` carrying `origin.kind` `task-notification`.
- Lines that aren't a JSON object, don't parse, or have a `type` not in `knownTypes` (`internal/claude/session.go`) go to `Options.OnBadLine`; lines that parse are still delivered as events whatever their type.
## Control channel
Either side sends `{"type":"control_request","request_id":ID,"request":{"subtype":...}}` and the other answers `{"type":"control_response","response":{"subtype":"success"|"error","request_id":ID,"response":{...},"error":"..."}}`.
Host → CLI:
- `initialize` must be first. It carries hook registrations: `{"hooks":{"Stop":[{"matcher":null,"hookCallbackIds":["hook_0"]}],...}}`.
- `interrupt` ends the current turn; the turn's `result` follows almost immediately and a running Bash tool is killed. The session stays usable.
2026-10-14 19:35:34 -07:00
- `set_model` (`{"subtype":"set_model","model":M}`) switches the running process's model for later turns.
- `set_permission_mode`, `mcp_status`, `get_context_usage`, `rewind_files`, `stop_task` exist and are not used yet.
CLI → host:
- `hook_callback` with `callback_id` and `input` (the hook's input JSON). The response body is the hook output. A Stop hook returning `{"decision":"block","reason":"..."}` keeps the agent going with the reason fed back to it.
- `mcp_message` with `server_name` and a JSON-RPC `message` for an `sdk`-type MCP server; answer with `{"mcp_response": <JSON-RPC response>}`. Notifications still need a response.
- `can_use_tool` only arrives when a permission prompt would show; under `bypassPermissions` it should not.
- `control_cancel_request` abandons an in-flight request.
## MCP tool loading
By default the CLI hides MCP tool schemas behind its `ToolSearch` tool, so an agent spends a model turn on `ToolSearch` before it can call a byre tool. `"alwaysLoad": true` in the server's `--mcp-config` entry puts byre's tools in the prompt from the first turn.