Extended thinking: signed thinking block 'cannot be modified' (400) permanently wedges session
Summary
With extended/interleaved thinking enabled, Claude Code intermittently replays a modified signed thinking block in the latest assistant message, producing:
API Error: 400 messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. These blocks must remain as they were in the original response.
Once this occurs the corrupted assistant message is persisted in the transcript, so every subsequent request re-sends it and fails identically. The session becomes permanently unrecoverable — re-prompting and "try again" both reproduce the same 400. A full /clear is the only reliable escape (/rewind to before the failed turn may also work).
Environment
- Reproduced across Claude Code 2.1.143, 2.1.150, 2.1.153 (currently on 2.1.154).
- macOS (darwin 25.5.0).
- Extended/interleaved thinking enabled.
Impact
Silent, repeated total loss of a working session on long-running tasks. Especially costly on Opus, where extended thinking is core to the product — the only durable user-side workaround is disabling thinking, which is a capability downgrade users shouldn't have to accept.
Evidence
Observed in 5 separate session transcripts. The error references a thinking block (the cited content.M is always a thinking block), and the same fingerprint recurs:
messages.19.content.21(recurs across multiple sessions)messages.1.content.7messages.327.content.14
(messages.N.content.M indexes the API request payload, not any project file.) The 400 is raised before model reasoning starts, so any "self-analysis / retry" inside the same session just resends the invalid transcript and fails again.
Reproduction
Trigger: a tool-heavy assistant turn while extended thinking is on, where the harness reconstructs that assistant message after it was first emitted. Observed concrete triggers:
- Background tasks — a
run_in_backgroundBash command whose async completion notification re-enters the originating assistant turn; on resume the harness replays that turn's thinking block modified → 400. - Parallel tool-call cancellation — a batch of parallel tool calls where one errors and cancels its siblings (
Cancelled: parallel tool call ... errored); the next turn 400s on the thinking block. - Plain tool-heavy turn — in one session the 400 fired immediately after a single file-write tool call on a turn that contained thinking blocks. So the trigger is more general than (1)/(2).
Expected behavior
Signed thinking / redacted_thinking blocks must be persisted and replayed byte-for-byte unmodified across all harness operations — including background-task resumption, tool-call cancellation, and normal multi-tool turns. The client should never emit a modified thinking block.
Actual behavior
The harness emits a modified thinking block on the latest assistant message, the API rejects it with a 400, and the session is permanently wedged with no in-session recovery.
15 Comments
I am seeing the same issue. I submitted via /feedback, details below so you can link them / find them from this issue.
---
Environment Info
Errors
Additional data point confirming this is not macOS- or model-specific.
Environment
Trigger (matches #63231)
A single long assistant turn (one
requestId, 37 content blocks) withthinkinginterleaved between manytool_useblocks, ending in parallel tool calls:Tools in that turn: 12×Bash, 3×Read, 2×Edit, 1×Write, 1×AskUserQuestion. So
AskUserQuestioninside a large interleaved-thinking turn reproduces it here too (cf. #63231).Error progression in the same session
The
content.Nindex grows with turn length, consistent with longer interleaved turns being more likely to hit it.On-disk vs in-flight finding (may help narrow the cause)
In the persisted transcript (
~/.claude/projects/.../<session>.jsonl), everythinkingblock is stored with a validsignaturebut an emptythinkingstring ("") — this is true for all my sessions, not just the broken one, so it appears to be normal persistence behavior (text dropped, signature kept).I tried editing the on-disk transcript to strip those empty
thinkingrecords (rewiringparentUuid); the session resumed and worked for short turns, but the 400 returned on the next long interleaved-thinking turn. So the corruption is in the in-flight/in-memory replay of the latest assistant turn, not in the persisted file — editing the transcript does not prevent recurrence. Confirms the "wedge" is regenerated live whenever a sufficiently long interleaved turn occurs.User-side workaround that holds: keep turns short (few tool calls each) so no single assistant turn accumulates a long interleaved thinking+tool sequence.
/clear/ new session escapes a wedged state, as already noted.I've been seeing the same issue.
我也遇到这个问题了 macos系统中的vscode软件中的claude code插件,版本 2.1.154,一直回复API Error: 400 Failed to deserialize the JSON body into the target type: messages[3].role: unknown variant system, expected user or assistant at line 1 column 80437
Independent confirmation that this is structurally sub-pattern 13D of Cluster 13 (Extended-Thinking Session Wedging) — and the most operationally serious of the four sub-patterns because the trigger surface is not pinned.
Cluster framing
The four sub-patterns share one root failure mode (thinking-block serialization not surviving the round-trip between the client and the API), but reach the failure through different code paths:
Field guide articulating all four sub-patterns with the recovery surface limits and the operator-side advisory hooks: Extended-Thinking Session Wedging — A 36-Hour Surge with 4 Sub-Patterns and Operator-Side Recovery Paths (MIT, ~2,800 words).
Why 13D is the largest by volume and the hardest to defend against
The reaction count on this specific issue is comparable to 13B and 13C (~6), but the duplicate-flagged cluster around it is the largest of the four sub-patterns. Aggregating this issue with #63199, #63072, #63231, #63341, #63121, #63078, #63346, #63239, #63337, #63247, #63213 gives at least 12 reports — likely 50+ reactions in total — describing the same failure without a 13A/B/C trigger.
@braklo's note that it reproduces on claude-opus-4-8 (not just 4-7), and the version-independent reproduction across v2.1.143, v2.1.150, v2.1.153, v2.1.154 documented across this thread, place 13D outside the model and outside any single release. The trigger surface is upstream of release boundaries — which means an operator-side advisory hook can warn but cannot block, since the failure can fire on any extended-thinking session regardless of the session's history.
The reporter's articulation — "the only durable user-side workaround is disabling thinking" — is the honest summary. Disabling extended thinking trades capability for session durability. There is no fourth path that keeps both.
Possible additional trigger surface (13E candidate)
One forum post I found during the research surfaced a fifth candidate sub-pattern not yet pinned to a GitHub issue: "When the text contains unpaired Unicode surrogates (which can occur when Claude reasons about binary data, code, or malformed strings), the surrogates are stripped, the text changes, and the cryptographic signature becomes invalid." If this trigger is real, it would explain some of the intermittency observed in 13D — sessions that reason about binary content, source code with unusual Unicode, or malformed strings would be more susceptible. I'm flagging it for visibility; it would benefit from a reporter who can reproduce on a session that hits binary/code-heavy reasoning.
Operator-side defenses (cc-safe-setup)
The hook shipped 2026-05-29 (PR #445, 54 tests passing) targets sub-pattern 13A at
SessionStart. The advisory for 13D is in design as aPostToolUsehook that scans the trailing transcript for the precursor shape (thinking blocks with empty text but non-empty signature) mid-session, ahead of the next resume — complementing the SessionStart hook by catching the shape before resume actually fires the 400. Rate-limited per session. Targeting a June 2026 ship.The honest limit on the 13D advisory: since the trigger is not pinned, the advisory has to fire on "extended thinking is active and the historical reproduction rate suggests caution," which is broader than the 13A/B/C advisories. Operators who want the strongest 13D defense should disable extended thinking for tasks that don't strictly require it, and accept the capability downgrade.
For sessions that have already wedged via any sub-pattern,
miteshashar/claude-code-thinking-blocks-fixis the recommended entry point — it operates on the on-disk transcript and produces a repaired version that can resume cleanly. The cluster's recovery surface is structurally narrow at the prevention layer; the repair layer is where the practical workflow recovers.What I'm watching for
The fastest signal for 13D would be a reproducer that's deterministic enough to pin the trigger surface — currently the 12+ reports describe symptoms but not preconditions. If a reporter can isolate the trigger to a specific kind of content (binary reasoning, certain Unicode ranges, specific tool sequences), the advisory layer can target it specifically rather than coarsely. I'm tracking the trigger-surface refinement in the cluster registry: cluster-tracker.html#cluster-extended-thinking-wedge.
Reproduces on 2.1.156, Windows 11, resuming an old multi-compaction session with
--model claude-opus-4-8 --resumeHitting the same bug. Adding a specific trigger combination not yet documented here:
Trigger sequence:
claude --model claude-opus-4-8 --resume <session>thinkingblocks at indices 36 and 38)messages.5.content.36/messages.5.content.38Environment:
claude-opus-4-8, extended/interleaved thinking ONConfirmed sticky behavior: rewinding to just before the failure re-sends the same corrupted assistant turn → identical 400. Only recovery was rewinding to before the entire offending assistant turn. Then the same 400 returned again on the next large interleaved-thinking turn.
Full local
.jsonltranscript available with sanitized excerpts on request.Additional data point: same 400 in non-interactive Agent SDK mode (no
/plan,/clear,/compact, or model-switch)Reproduced this exact failure in a headless CI environment, which narrows the trigger: it is not specific to interactive context operations. The harness here is the official GitHub Action wrapping the bundled
@anthropic-ai/claude-agent-sdk— a single non-interactive agent run, started fresh, with no compaction/clear/model-switch and no plan mode.Error (identical signature):
request_id: req_011CbYAd4LaUy9vhzyrkCTKNclaude-opus-4-8, Linux x64 runner, extended/interleaved thinking enabled, single uninterrupted agent loop (no/clear,/compact,/model, or/plan— none of those concepts exist in this non-interactive path).Differential evidence (most useful part): the same harness, same model, same system prompt was run against two different inputs:
| | run A (passes) | run B (fails) |
|---|---|---|
|
is_error| false | true ||
num_turns| 11 | 39 |The failure correlates with transcript length / tool-call density (run B does many more tool-heavy assistant turns), pushing the first assistant message past ~28 content blocks. The 400 then fires from
readMessageswhile reconstructing that long transcript for the next request — block 28 is athinkingblock that gets modified during reconstruction. A short transcript (11 turns) never reaches the buggy path.This suggests the root cause lives in the SDK's transcript replay/serialization (
readMessages) and is reachable on any sufficiently long tool-heavy turn — independent of/plan, compaction, or model switching.@tlmader — The non-interactive Agent SDK confirmation is the most important data point this thread has added on the trigger surface so far. It rules out the entire "interactive operation" hypothesis that the early thread (myself included) had been working with —
/plan,/clear,/compact, model-switch were red herrings. The trigger is the round-trip of a thinking-block signature through any path that touches transcript reconstruction.This adds a fifth sub-pattern to the four I documented earlier:
@anthropic-ai/claude-agent-sdk(including the officialclaude-code-actionGitHub Action wrapper) hit the same 400 "thinking blocks cannot be modified" when the transcript reconstructed for a new tool round-trip references a signature whose accompanying text is empty./rewind, no/compact, no session to abandon — the SDK either succeeds the round-trip or fails permanently for that invocation. CI pipelines can retry the whole job, but the same input reproduces the same failure.matcher:""PreToolUse hook on the calling side cannot reach the SDK's internal serialization layer. The only operator-side workaround I can see is to short-circuit the SDK invocation with retries that downgrade the model to a non-extended-thinking config (e.g.,--model claude-sonnet-4-6instead ofclaude-opus-4-8) — but this loses the entire extended-thinking value the harness was paying for.This makes 13E the most operationally serious of the five sub-patterns: there is no operator-side recovery without sacrificing the feature you're invoking the SDK for. The implication for the upstream fix is that it has to live in the SDK's transcript reconstruction layer, not in any one of the interactive entry points. A fix that only patches
/compactor the auto-rebuild path inside the CLI will leave the SDK surface unaddressed, and CI pipelines will continue to hit this in production.For CI environments hitting this today, the least-bad mitigation I've seen is a wrapper around the SDK invocation that catches the 400 with the specific "thinking blocks cannot be modified" body and re-invokes without
thinking_configset — accepts the productivity loss to keep the pipeline green. Far from ideal but it converts a permanent wedge into a degraded-quality completion.@tlmader and thread — Correction on my earlier reply: I labeled the non-interactive Agent SDK surface "sub-pattern 13E", but my own prior tracking had already assigned 13E to the ToolSearch trigger surface (a different mechanism documented at https://github.com/anthropics/claude-code/issues/63147#issuecomment-4580358273). To avoid number collision with the existing nomenclature, I'm renaming the SDK/CI surface to sub-pattern 13H — Non-interactive SDK / CI surface.
The substance of the analysis stands; only the label changes. Corrected sub-pattern map (as of 2026-05-30):
extended-thinking-resume-warning, PR #445)opus48-thinking-wedge-advisor)Apologies for the number churn. The 13H analysis itself is unchanged: there is no operator-side recovery without sacrificing the extended-thinking feature the harness was invoking, and the upstream fix has to be in the SDK's transcript reconstruction layer — not in any of the interactive entry points where the existing fixes for 13A-D land. CI pipelines hitting the SDK surface will not be addressed by a
/compactpatch or the auto-rebuild path inside the CLI.The 7-pattern split (13A through 13H, with 13E ToolSearch occupying the slot I incorrectly reused) is being tracked at cluster-tracker.html for the cc-safe-setup repo. The current shipped-hook coverage is 13A and 13G; 13B-D have advisory hooks in design, and 13E/F/H have no operator-side intervention surface yet beyond the workarounds already documented in this thread.
it doesn't permanently wedge my session but running into it is annoying as fuck
Adding a sanitized data point that looks related, but presents as a silent/malformed-tool stall rather than always surfacing the 400 directly.
Environment
claude-opus-4-8[1m]high; risk appears higher withxhigh/maxObserved behavior
In a resumed interactive session, a natural user request that combines brief reasoning + tool use + final text can produce this sequence:
thinkingandstop_reason=tool_use.tool_useblock is emitted for Claude Code to execute.Your tool call was malformed and could not be parsed. Please retry.text.So the user-visible failure can look like an empty reply / stuck thinking turn even when no 400 is shown in the UI.
A/B notes
claude -ptests with Opus 4.8 high can succeed for short replies and Bash tool use, so this does not look like a general API outage.DISABLE_INTERLEAVED_THINKING=1was not sufficient in the interactive/resumed route.--thinking disabledavoided the malformed-tool path and produced valid Bashtool_use+ final text, but that is a capability downgrade.claude-opus-4-6[1m]with max effort.This seems consistent with the existing reports around extended/interleaved thinking + tool use corrupting or wedging the latest assistant turn. I can provide more sanitized transcript structure if useful, but cannot post private chat content publicly.
the part that bites isn't the thinking text, it's the signature. the server signs the block over its exact serialized bytes, so the client has to replay it verbatim. the moment a harness round-trips it through its own typed message model (parse to a struct, drop a null field, reorder keys on re-serialize) the bytes drift and the signature stops validating, even though the visible content is identical. background-task resume and parallel-cancel both force that re-serialize on the latest assistant turn, which is why those two repro most. fix is to stash the raw block keyed by id and splice it back unmodified, never rebuild it from parsed fields.
@m13v — this is the cleanest articulation of the root mechanism this thread has produced. The signature is over exact serialized bytes, not over content the harness sees. Frame it that way and the whole sub-pattern map collapses to one invariant violation, with each documented sub-pattern just a different trigger path forcing re-serialization on the latest assistant turn.
Updating the Cluster 13 articulation against your framing:
Root mechanism (single, shared across 13A–D and 13H)
thinkingblock over its emission-time canonical byte sequence.Why each sub-pattern reaches it
The five established sub-patterns all force a parse → struct → re-serialize round-trip on the latest assistant turn, but through different code paths:
tool_useblocks; surroundingthinkingblocks get re-serialized as a side effect even though they are not the target of the edit. Matches your "parallel-cancel forces that re-serialize" framing directly — the cancel target is thetool_use, but the byte drift lands on the adjacentthinking.Fix shape, restated against your framing
This is the only fix that survives all sub-patterns at once because it sidesteps the byte-drift class entirely:
thinkingblock's raw bytes alongside its block id (or position index if no id).bytes(block_after_roundtrip) == bytes(block_at_creation)keyed by id, across the entire transcript-reconstruction surface (resume, cancel, parallel-batch unwind, SDK request loop).The current implementation appears to rely on JSON-level identity-by-content, which is exactly the assumption byte-identity breaks.
Implication for harness designers (anyone building on
@anthropic-ai/claude-agent-sdkor the Anthropic SDK directly)If you build any wrapper that observes or transforms assistant messages, treat
thinkingblocks as opaque, byte-stable, identity-keyed — pass-through only. Any parse/re-serialize layer above the SDK reintroduces the same hazard the SDK is trying to avoid; byte stability has to be end-to-end.This is also the honest framing for operator-side mitigations. The cc-safe-setup hook for 13A (
extended-thinking-resume-warning, PR yurukusa/cc-safe-setup#445) detects the trigger surface but cannot fix 13D/13H, because the trigger there is the SDK's own request loop. The only universally reliable operator-side mitigation across all sub-patterns remainsclaude --model claude-opus-4-7(which avoids signed thinking blocks entirely) for sessions where reliability matters more than reasoning depth.Updating linked artifacts to credit your framing:
cluster-tracker.htmlCluster 13 entry — same revision, byte-identity framing as the lede.Thanks for the depth — this is the kind of structural framing that lets the cluster argument actually converge on a single fix instead of one mitigation per sub-pattern.
@m13v @yurukusa thanks, the byte-level signature explanation makes sense and matches what we observed.
Adding our sanitized launch details in case they help narrow which Claude Code path is re-serializing the latest assistant turn:
Notes:
claude -p.thinkingblocks. It only drives the CLI and watches the transcript/output.thinkingandstop_reason=tool_use, but no validtool_useblock appeared for the harness to execute. Claude Code then injectedYour tool call was malformed and could not be parsed. Please retry., and the turn could remain in thinking with no final visible text.claude -ptests with the same model could pass; the issue showed up in the interactive/resumed route.DISABLE_INTERLEAVED_THINKING=1was not sufficient for our route.--thinking disabledmade the same project-like tool tests produce valid tool calls and final text, but it removes the signed-thinking surface entirely, so it is only a degraded workaround.Questions:
--resumepath for a long interactive session force a parse -> typed model -> re-serialize round trip on the latest assistant message, even if the app wrapper never edits the transcript?--thinking disabledcurrently the only reliable workaround for this class of signed-thinking replay issue, or shouldDISABLE_INTERLEAVED_THINKING=1be expected to avoid it if applied correctly?thinking,stop_reason=tool_use, missingtool_use, recovery prompt, timing), or is that enough to confirm this is the same raw signed-block replay problem?@dankefox — the launch details are exactly what narrows it. I maintain cc-safe-setup (community side, not Anthropic), so treat Q1/Q3 as evidence-based inference rather than a maintainer confirmation — but your data answers most of it on its own.
Q1 — does
--resumeforce a parse → typed-model → re-serialize round-trip even though your wrapper never edits the transcript? Your own A/B is the discriminating evidence: cleanclaude -ppasses, the long-running--resumeroute fails, same model. Since your wrapper only drives the CLI and watches output (never rebuildsthinkingblocks), the re-serialization can't be coming from your layer — it has to be inside Claude Code's resume reconstruction. The--resumepath rebuilds the latest assistant turn from the parsed transcript to continue it; that rebuild is the parse→struct→re-serialize step m13v described, and-pskips it because there's no prior turn to reconstruct. So your wrapper is exonerated, and the round-trip is internal to the resume path. That's consistent with why background-task resume and parallel-cancel repro most — both force a rebuild of the latest assistant turn.Q2 — is
--thinking disabledthe only reliable workaround, or shouldDISABLE_INTERLEAVED_THINKING=1avoid it?DISABLE_INTERLEAVED_THINKING=1is not reliable for this class, and your route confirms it. cnighswonger's 2.1.148 disassembly (cited in the Cluster 13 tracker entry) shows that flag does not actually remove the signed-thinking surface, so the byte-drift hazard survives it. Two things that do work, with different costs:--thinking disabled— removes the signed-block surface entirely, so it's reliable but degraded (you lose thinking).--model claude-opus-4-7(orclaude-opus-4-6[1m]). The longitudinal signature data on #61133 / #62123 shows older generations carry zero "retry also failed" events for the same workflow, while it worsens 4.7 → 4.8. You're onclaude-opus-4-8[1m], so dropping to 4.7 keeps signed thinking and lands on a generation with far fewer replay failures — usually the better trade than killing thinking outright.Q3 — is the block structure enough, or would maintainers need more? I can't speak for the maintainers. For classification into this cluster, what you have (thinking +
stop_reason=tool_use+ missingtool_use+ recovery prompt + timing) is already strong and matches the signature. The one piece that would let anyone confirm the re-serialize hypothesis definitively rather than infer it is a byte-level diff of the samethinkingblock keyed by id: emission-time bytes vs the bytes replayed on the resume turn. If your wrapper watches the transcript, capturing those two byte sequences for one failing block (even with the text content redacted — it's the byte/whitespace/key-order drift that matters, not the content) is the decisive artifact. That's the evidence that turns "the signature stopped validating" from a hypothesis into a demonstrated invariant violation.Folding your route (PM2 → tmux →
--resumeinteractive, wrapper-doesn't-touch-transcript) into the Cluster 13 evidence as the cleanest "the re-serialize is internal to resume, not the wrapper" data point. Thanks for the sanitized detail — the "claude -pclean /--resumefails" contrast is the part that pins it.