CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-05-28Intermediate

Why JSON.parse Fails on Claude API Streaming tool_use Arguments — and How to Fix It

When you stream a Claude API response with tool_use, calling JSON.parse on each input_json_delta throws SyntaxError. Here is the correct way to assemble partial_json fragments, plus disconnect handling.

Claude API115streaming21tool use5JSONtroubleshooting87

I have been shipping iOS apps as an indie developer since 2014, and the first thing that bit me when wiring Claude API into a production chatbot was streaming responses that contained tool_use. If you call messages.create({ stream: true }) and try to JSON.parse each event as it arrives, you will hit SyntaxError: Unexpected end of JSON input almost every time. The sample code looks fine, the docs look fine, and yet nothing works. Here is what is actually happening and what to do about it.

Symptom: parsing input_json_delta throws SyntaxError on every other call

If you log the raw events, the stream looks like this:

{ "type": "message_start", "message": { ... } }
{ "type": "content_block_start", "index": 0, "content_block": { "type": "tool_use", "id": "toolu_01...", "name": "get_weather", "input": {} } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "{\"loc" } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "ation\": \"" } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "Tokyo\"}" } }
{ "type": "content_block_stop", "index": 0 }
{ "type": "message_delta", "delta": { "stop_reason": "tool_use" } }
{ "type": "message_stop" }

Try to parse {"loc and the parser throws. Each partial_json chunk is not a JSON fragment — it is an opaque slice of characters that becomes valid JSON only after every chunk inside the same index is concatenated and the block has stopped. The model may split a chunk mid-key, mid-value, even mid-escape.

The implementation that "parses each delta as it arrives" appears to work in small examples because everything happens to fit in one event. Push the input past a few hundred tokens, or enable parallel tool use, and it falls over.

Root cause: the API contract is per-block, not per-delta

The Anthropic messaging docs spell this out under input_json_delta: concatenate every partial_json value, keyed by content_block_delta.index, and parse the whole buffer once you see content_block_stop for that index. The temptation to treat partial_json like text_delta (which you can stream one character at a time into the UI) is what causes the bug.

The three failure modes I have hit on real apps:

  • A travel booking flow received a date range across three deltas, and the code parsed the first chunk "2026-06- and crashed
  • Parallel tool calls (default behaviour on Sonnet 4.6 and newer) interleaved index: 0 and index: 1 deltas, and a single shared buffer mixed the bytes
  • A multi-turn conversation persisted half-built tool arguments to a cookie, because the writer fired on every delta event instead of on content_block_stop

The fix: one buffer per index, parse only on stop

Here is the minimum implementation that works in production. It uses the official @anthropic-ai/sdk (Node.js), but the same logic applies if you parse raw SSE yourself.

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
type ToolBuffer = {
  id: string;
  name: string;
  json: string; // accumulate partial_json in index order
};
 
async function callWithStreamingTools() {
  const stream = client.messages.stream({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    tools: [/* ... */],
    messages: [{ role: "user", content: "Weather in Tokyo and tomorrow's headlines?" }],
  });
 
  const buffers = new Map<number, ToolBuffer>();
  const completedToolUses: Array<{ id: string; name: string; input: unknown }> = [];
 
  for await (const event of stream) {
    if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
      buffers.set(event.index, {
        id: event.content_block.id,
        name: event.content_block.name,
        json: "",
      });
    } else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
      const buf = buffers.get(event.index);
      if (buf) buf.json += event.delta.partial_json;
    } else if (event.type === "content_block_stop") {
      const buf = buffers.get(event.index);
      if (buf) {
        // Parse here, never earlier
        const input = buf.json.length === 0 ? {} : JSON.parse(buf.json);
        completedToolUses.push({ id: buf.id, name: buf.name, input });
        buffers.delete(event.index);
      }
    }
  }
 
  return completedToolUses;
}

Three things matter:

  1. Allocate a buffer only when content_block_start reports tool_use. Do not share a buffer with text blocks
  2. Use += on partial_json. Never call JSON.parse until the block is complete
  3. On content_block_stop, fall back to {} if the buffer is empty — tools that take no arguments still arrive as an empty input field

After fixing only this, the 500-level errors that appeared roughly once a week on my automated reply pipeline disappeared.

Handling disconnects mid-block

Streaming is also a fight with flaky networks. While instrumenting AdMob revenue summaries for one of my wallpaper apps, I saw SSE connections drop the moment the device came back from airplane mode, and content_block_stop never arrived. The right defence is to treat unfinished buffers as toxic and discard them at the boundary.

try {
  for await (const event of stream) { /* logic above */ }
} catch (err) {
  // Connection lost. Treat anything still in the buffer as incomplete
  for (const [index, buf] of buffers) {
    console.warn(`incomplete tool_use index=${index} name=${buf.name} bytes=${buf.json.length}`);
  }
  throw err;
}
 
// On normal completion, always inspect the final stop_reason
const final = await stream.finalMessage();
if (final.stop_reason !== "tool_use" && final.stop_reason !== "end_turn") {
  // max_tokens may have truncated the response. Consider retry or continuation
}

If you hand a half-built tool_use to the rest of the application, downstream schema validation throws confusing errors like "required field location missing". Refusing to forward incomplete tool calls saves a lot of debugging time.

SDK convenience events vs. raw SSE

The official SDK exposes higher-level events on messages.stream(...), including on("inputJson", (delta, snapshot) => ...) which hands you the fully concatenated string on every update. If your UI does not need to render the partial arguments live, lean on those events instead of writing the buffer logic by hand.

For environments such as Cloudflare Workers where you parse fetch ReadableStream output directly, watch for these traps:

  • SSE frames are delimited by \n\n — make sure you split on that, not on \n
  • A single frame can contain several events when small deltas arrive back to back
  • Strip the data: prefix before calling JSON.parse on the event line
  • partial_json values can contain escaped newlines; treat them as plain string data and concatenate without modification

When I last ran this on Workers I tried the SDK's BetaMessageStream before reaching for a hand-rolled SSE parser. The bundle size cost was worth the easier production debugging.

A debugging order that converges fast

When something is broken end to end, this is the order I work through:

// 1) Dump raw events untouched
for await (const event of stream) {
  console.log(JSON.stringify(event));
}

Look at the log and verify, in order:

  • Does each content_block_start show content_block.type === "tool_use"? Mixing in a text block silently corrupts the buffer
  • If you string-concat every partial_json for a given index, do you get valid JSON? Pipe to jq to confirm
  • Does a message_delta.stop_reason arrive after the last content_block_stop?
  • For parallel tool calls, are the index values contiguous (0, 1, 2 ...) ?

If those four boxes are ticked, the remaining bugs are buffer-keying mistakes. Trust the raw event stream before you rewrite the parser; that is the shortest path to a fix in my experience.

One more pitfall: parallel tool use and partial continuations

Sonnet 4.6 and newer return parallel tool_use blocks by default. Setting disable_parallel_tool_use: true forces serialisation and your buffer code keeps working unchanged — only index 0 ever appears. The other direction is the dangerous one: if you keep parallel calls enabled and try to optimise by ignoring later blocks once the first one finishes, you will miss content_block_stop for blocks that arrive later in the stream. The safest pattern is to receive every tool_use, then submit every tool_result in one follow-up message.

The shape of the API does shift across SDK versions, so I make a habit of dumping fresh raw events before each production release. I hope this saves someone else an evening of squinting at logs.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-07-11
Tightening Tool Schemas From the Arguments You See in Production
Record the arguments Claude actually passes to your tools in production, then use that distribution to add enums and patterns back into your JSON Schema. With logging code and before/after numbers.
API & SDK2026-06-29
Let Claude Actually See the Images Your Tools Return — Use Image Blocks in tool_result and Cut Tokens by Roughly 10x
Stuffing a base64 string into a tool_result makes the same image cost roughly 10–20x more tokens. Here is how to return it as an image content block instead, with SDK code, a token-cost estimate, and the gotchas I hit in production.
API & SDK2026-06-28
Measure Streaming CPU and Dropped Chunks to Stabilize Long Batch Jobs
You start an overnight batch, and by morning only half of it finished. The culprits were CPU pinned during streaming and a quiet connection drop. Here is a monitor wrapper that measures stream CPU and throughput, and resumes from interruptions.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →