The stream-json protocol turns the Claude CLI into a real-time event emitter. Instead of waiting for the entire response, you get NDJSON events as they happen — init metadata, token-by-token text, tool calls, tool results, rate limits, and the final summary. Combined with --input-format stream-json, it enables bidirectional communication for building real-time UIs, agent chains, and monitoring dashboards.
Enabling Streaming
Two flags are required to activate the stream protocol: --output-format stream-json sets the output mode, and --verbose ensures the init event with full session metadata is emitted. Without --verbose, the init event may be missing — always pair the two together.
Each line in the output is a complete, independent JSON object. A simple prompt without tool use produces exactly four events: system (init), assistant (response), rate_limit_event (API status), and result (final envelope).
Event Types
Every event has a type field that tells you what it represents. Here is the complete catalog:
Stream Event Types
| Event Type | Subtype / Trigger | When It Fires | Key Fields |
|---|---|---|---|
system | init | First event in every stream | tools, mcp_servers, model, session_id, permissionMode |
system | api_retry | On retryable API error (rate limit, server error) | attempt, max_retries, retry_delay_ms, error |
assistant | — | After the model generates a response | message.content[], message.usage |
user | — | After a tool executes and returns results | Tool result content |
rate_limit_event | — | After each API call (always fires, even when allowed) | rate_limit_info.status, resetsAt |
result | success or error_* | Last event in the stream | Same fields as —output-format json envelope |
stream_event | message_start, content_block_delta, message_stop, … | Only with —include-partial-messages | event.delta.text — incremental token chunks |
When Claude uses tools, events interleave: an assistant event contains the tool call (content[].type === "tool_use"), followed by a user event with the tool result. This cycle repeats for each tool invocation before the final assistant response.
Init Event
The first event in every stream is a system/init payload containing session metadata. This is the richest event in the protocol and the key to initializing any UI or monitoring system.
Use the init event to power health checks (verify MCP server connections), tool audits (confirm allowlists), version pinning (check claude_code_version), and UI initialization (populate tool lists and model names).
Partial Messages
By default, you get the complete response in a single assistant event. Adding --include-partial-messages inserts stream_event entries for each token chunk, enabling typewriter-style UIs that render text as it is generated.
The sequence of stream_event entries follows a strict order:
message_start— model ID, empty contentcontent_block_start— new text block begins (index 0)content_block_delta— incremental tokens:"\n\nHello", then"! How", then" can I help you today?"content_block_stop— block finishedmessage_delta—stop_reason: "end_turn", final usage statsmessage_stop— stream complete
The full assistant event (with the complete assembled message) appears after all the deltas. For a typewriter UI, consume content_block_delta events in real time — do not wait for the assistant event.
—include-partial-messages is high volume. A 500-word response can generate hundreds of stream_event lines. Only enable it for typewriter UIs, not data pipelines where you only need the final result.
Bidirectional Communication
The stream protocol is not output-only. Adding --input-format stream-json lets you send messages to Claude via stdin while receiving events on stdout — enabling real-time UIs, agent orchestration systems, and custom IDE integrations.
# Full bidirectional modeclaude -p --input-format stream-json --output-format stream-json --verboseThere are strict pairing rules for these flags:
Input/Output Flag Requirements
| Flag | Requires | Purpose |
|---|---|---|
—input-format stream-json | —output-format stream-json | Accept stream-json on stdin for follow-up messages |
—replay-user-messages | Both —input-format and —output-format set to stream-json | Echo user messages back on stdout for acknowledgment |
You can also chain Claude instances by piping stream-json output from one into another:
# First Claude analyzes, second Claude summarizesclaude -p "Analyze auth.py" --output-format stream-json --verbose | \ claude -p "Summarize the analysis" --input-format stream-json --output-format stream-json --verboseThe second instance receives all events from the first — including tool results and the final response — as context for its own task.
Real Stream Payload
Here is the complete four-event stream from a real CLI call. This is exactly what --output-format stream-json --verbose produces for a simple prompt with no tool use.
In a real stream, each of these four objects would be on its own line with no commas or brackets between them. The nested structure shown above groups them for readability — the actual NDJSON output is one JSON object per line.
Parsing Stream Events
Here is a Node.js generator that spawns a Claude process and yields parsed events as they arrive:
import { spawn } from 'child_process';import { createInterface } from 'readline';
async function* streamClaude(prompt, { partial = false } = {}) { const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose']; if (partial) args.push('--include-partial-messages');
const proc = spawn('claude', args); const rl = createInterface({ input: proc.stdout });
for await (const line of rl) { if (line.trim()) yield JSON.parse(line); }}
// Dispatch on event typefor await (const event of streamClaude('Explain recursion')) { switch (event.type) { case 'system': if (event.subtype === 'init') { console.log(`Model: ${event.model}, Tools: ${event.tools.length}`); } break; case 'assistant': console.log(`Response: ${event.message.content[0].text}`); break; case 'result': console.log(`Cost: $${event.total_cost_usd.toFixed(4)}`); break; }}For a typewriter effect, enable partial messages and filter for text deltas:
process.stdout.write('Claude says: ');for await (const event of streamClaude('Write a haiku', { partial: true })) { if (event.type === 'stream_event') { const delta = event.event?.delta; if (delta?.type === 'text_delta') { process.stdout.write(delta.text); } }}console.log();Retry Events
When the API returns a retryable error (rate limit or server error), a system/api_retry event appears before the retry attempt. Use this to show retry progress in your UI or implement custom backoff logic.
{ "type": "system", "subtype": "api_retry", "attempt": 1, "max_retries": 5, "retry_delay_ms": 2000, "error_status": 529, "error": "server_error"}—verbose is required with —output-format stream-json. Without it, the init event with session metadata may not be emitted. Always use both flags together: —output-format stream-json —verbose.
Each line is independent JSON — this is NDJSON, not a JSON array. There are no commas between objects and no wrapping brackets. Parse line by line with JSON.parse(line). Calling JSON.parse(entireOutput) on the full stream will fail.
The result event is always last and contains the same data as —output-format json. You get both real-time streaming events and the final summary envelope in one stream — no need to run a second call to get the result metadata.