Bug: Interrupting a turn mid-thinking persists an unsigned `thinking` block to session JSONL, causing permanent `400 ... thinking blocks ... cannot be modified` on resume/next request
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Bug: Interrupting a turn mid-thinking persists an unsigned thinking block to session JSONL, causing permanent 400 ... thinking blocks ... cannot be modified on resume/next request
Environment
- Claude Code: 2.1.154
- Node: v20.20.2
- OS: Linux 7.0.1-070001-generic
- Model: Opus 4.8 (extended/interleaved thinking enabled)
Summary
When a turn is interrupted while the model is still streaming a thinking block, Claude Code persists that block to the session .jsonl without a signature. On the next API request the unsigned block is replayed and the API rejects the whole request with:
API Error: 400 messages.<i>.content.<n>: `thinking` or `redacted_thinking` blocks in the latest
assistant message cannot be modified. These blocks must remain as they were in the original response.
The session is then wedged: every subsequent request re-sends the same unsigned block and 400s again. /rewind recovers (it discards the offending message); a plain retry/--resume does not.
This appears to be the same family as #50375 but a distinct variant: #50375 describes empty messages in the JSONL; this is a non-empty assistant message containing an unsigned thinking block.
What I verified directly (from local session files)
- Across
~/.claude/projects/**/*.jsonlon this machine: 64thinkingblocks with nosignaturefield, spread over 20 session files. (Most thinking blocks are correctly signed — e.g. one file had 166 thinking blocks, 5 unsigned.) - Claude Code stores one content block per JSONL record (every assistant record I scanned had a single-element
content[]). The multi-block assistant message sent to the API is reassembled from these per-record blocks at request time. This is consistent with the error citing a highcontent.<n>index (e.g.content.33) on a single logical assistant message. - After converting the unsigned blocks to
text, the affected (idle) sessions no longer reproduce the 400, and the previously-wedged active session continued appending valid records normally (file stayed 100% valid JSON and grew by 16 records post-edit).
What I am inferring (not directly proven)
- Root cause = interrupt before the
signature_delta. The streaming API emits a thinking block'ssignatureat the end of the block (asignature_deltajust beforecontent_block_stop). My inference is that interrupting mid-thinking captures the thinking text but never the signature, so an unsigned block is persisted. I did not capture stream-level timestamps proving the exact interrupt point; the evidence is (a) unsigned blocks only occur on a minority of turns, and (b) they correlate with interrupted turns in my sessions. Treat the precise trigger as a strong hypothesis, not a confirmed fact.
Steps to reproduce (expected, from the hypothesis)
- Start a session with extended/interleaved thinking enabled.
- Send a prompt that induces a long thinking block.
- Interrupt (Esc / Ctrl-C) while the thinking block is still streaming, before the model emits its first tool_use/text.
- Send any new message (or
--resumelater). - Observe the
400 ... thinking blocks ... cannot be modifiederror.
Suggested fixes (in order of preference)
- Never persist an unsigned
thinkingblock. On interrupt, drop anythinkingblock that has not yet received itssignature(or persist it as a non-thinkingblock) so it is never replayed to the API. - Sanitize on the way out. When rebuilding a request, strip/convert any
thinkingblock lacking asignaturebefore sending (and avoid leaving an emptycontent[], which is the #50375 failure mode). - Self-heal on 400. When the API returns this specific error, locate and remove the offending unsigned block automatically instead of leaving the session wedged (today only
/rewindrecovers).
Note: editing the active session file races Claude Code's own writer; do it from outside the live session or after /rewind.
Related
- #50375 (empty-message variant + JSONL repair discussion)
What Should Happen?
Successive tool calls should not truncate thinking blocks
Error Messages/Logs
Steps to Reproduce
Steps to reproduce (expected, from the hypothesis)
- Start a session with extended/interleaved thinking enabled.
- Send a prompt that induces a long thinking block.
- Interrupt (Esc / Ctrl-C) while the thinking block is still streaming, before the model emits its first tool_use/text.
- Send any new message (or
--resumelater). - Observe the
400 ... thinking blocks ... cannot be modifiederror.
Claude Model
None
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.154
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Terminal.app (macOS)
Additional Information
_No response_
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗