claude-code - 💡(How to fix) Fix 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

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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.

Error Message

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.

Root Cause

What I am inferring (not directly proven)

  • Root cause = interrupt before the signature_delta. The streaming API emits a thinking block's signature at the end of the block (a signature_delta just before content_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.

Code Example

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.

---
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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/**/*.jsonl on this machine: 64 thinking blocks with no signature field, 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 high content.<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's signature at the end of the block (a signature_delta just before content_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)

  1. Start a session with extended/interleaved thinking enabled.
  2. Send a prompt that induces a long thinking block.
  3. Interrupt (Esc / Ctrl-C) while the thinking block is still streaming, before the model emits its first tool_use/text.
  4. Send any new message (or --resume later).
  5. Observe the 400 ... thinking blocks ... cannot be modified error.

Suggested fixes (in order of preference)

  1. Never persist an unsigned thinking block. On interrupt, drop any thinking block that has not yet received its signature (or persist it as a non-thinking block) so it is never replayed to the API.
  2. Sanitize on the way out. When rebuilding a request, strip/convert any thinking block lacking a signature before sending (and avoid leaving an empty content[], which is the #50375 failure mode).
  3. 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 /rewind recovers).

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)

  1. Start a session with extended/interleaved thinking enabled.
  2. Send a prompt that induces a long thinking block.
  3. Interrupt (Esc / Ctrl-C) while the thinking block is still streaming, before the model emits its first tool_use/text.
  4. Send any new message (or --resume later).
  5. Observe the 400 ... thinking blocks ... cannot be modified error.

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

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix 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