49 lines
3.3 KiB
Markdown
49 lines
3.3 KiB
Markdown
# 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. The CLI version the tests last passed against is in the `system`/`init` event of any transcript under `internal/claude/testdata/transcripts/` (`claude_code_version`).
|
|
|
|
## Launch
|
|
|
|
```
|
|
claude --output-format stream-json --input-format stream-json --verbose \
|
|
--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
|
|
|
|
- User turn, written to stdin: `{"type":"user","message":{"role":"user","content":"..."},"parent_tool_use_id":null,"session_id":"..."}`.
|
|
- 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).
|
|
- The `init` event arrives with the first turn, not at launch, so the session ID is unknown until the first message is sent.
|
|
|
|
## 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.
|
|
- `set_model`, `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 tools are deferred
|
|
|
|
The CLI hides MCP tool schemas behind its `ToolSearch` tool: an agent calls `ToolSearch` with `select:mcp__byre__report` before it can call `report`, costing an extra model turn every session.
|