Skip to content

Gotchas

Non-obvious behaviors in the stream-json protocol that will bite you if you don't know about them.

Double-Encoded result Field

The result field in result and system/result events is a JSON string inside JSON. It's not a plain string — it's a string that has been JSON.stringify()'d an extra time.

What It Looks Like

json
{
  "type": "result",
  "result": "\"Here is the summary of changes...\""
}

Note the escaped quotes inside the string value.

How to Parse

typescript
const raw = event.result        // '"Here is the summary of changes..."'
const actual = JSON.parse(raw)  // 'Here is the summary of changes...'

Why It Happens

Claude Code internally JSON.stringify()s the result text, then the NDJSON serialization JSON.stringify()s the entire event object — double-encoding the string.

TIP

The Translator handles this automatically. You only need to worry about this if you're working with raw ClaudeEvent objects.

Polymorphic tool_result.content

The content field in tool_result blocks has three possible shapes:

Shape 1: Plain String

json
{ "type": "tool_result", "tool_use_id": "toolu_1", "content": "file contents here" }

Shape 2: Array of Text Blocks

json
{
  "type": "tool_result",
  "tool_use_id": "toolu_1",
  "content": [
    { "type": "text", "text": "File edited successfully" }
  ]
}

Shape 3: Null

json
{ "type": "tool_result", "tool_use_id": "toolu_1", "content": null }

Handling All Three

Use extractContent():

typescript
import { extractContent } from 'claude-code-parser'

extractContent("hello")                                    // → "hello"
extractContent([{ type: "text", text: "hello" }])          // → "hello"
extractContent(null)                                        // → ""

Or if you're using the Translator, it normalizes this automatically — ToolResultEvent.output is always a plain string.

Thinking Block Field Name

The thinking content block uses different field names depending on the Claude Code version:

json
// Version A: uses "thinking" field
{ "type": "thinking", "thinking": "Let me analyze..." }

// Version B: uses "text" field
{ "type": "thinking", "text": "Let me analyze..." }

Always check both:

typescript
const text = block.thinking ?? block.text ?? ''

The Translator handles this automatically.

system/result vs result

Both of these can appear as turn-completion events:

json
// Legacy path
{ "type": "system", "subtype": "result", "session_id": "abc" }

// Current path
{ "type": "result", "subtype": "success", "session_id": "abc", "total_cost_usd": 0.02 }

The top-level result event has more fields (cost, usage, duration). The system/result path is legacy but still appears in some versions.

The Translator handles both and emits turn_complete or error for either.

Token Math

The modelUsage object in result events breaks tokens into four categories:

FieldWhat It Is
inputTokensFresh tokens sent to the model
cacheReadInputTokensTokens served from Anthropic's prompt cache
cacheCreationInputTokensTokens written to prompt cache (counts as input)
outputTokensTokens generated by the model

Total input tokens used for billing:

total = inputTokens + cacheReadInputTokens + cacheCreationInputTokens

The Translator computes this automatically in TurnCompleteEvent.inputTokens.

Cache tokens are cheaper

cacheReadInputTokens are billed at a lower rate than inputTokens. The total_cost_usd field already accounts for this. If you're displaying token counts, you may want to break them out separately.

Empty assistant Events

Claude Code may emit assistant events with empty content arrays:

json
{ "type": "assistant", "message": { "content": [] } }

Or with a null/missing message field:

json
{ "type": "assistant" }

Both are valid and should be silently skipped. The Translator handles this.

stop_reason Values

The stop_reason field in assistant messages indicates why Claude stopped generating:

ValueMeaning
end_turnClaude finished its response naturally
tool_useClaude wants to call a tool (waiting for approval/result)
max_tokensHit the output token limit

This field is informational — the Translator doesn't use it, but it's available on the raw ClaudeMessage if you need it.

Rate Limit Events

Claude Code may emit rate_limit_event objects on stdout:

json
{ "type": "rate_limit_event", ... }

These are silently skipped by the Translator. If you need to handle rate limiting, check for this event type in raw ClaudeEvent objects before passing them to the translator.

Not affiliated with or endorsed by Anthropic.