A diagnostic and repair tool for Claude Code sessions that become permanently stuck on the API error thinking or redacted_thinking blocks in the latest assistant message cannot be modified.
- Have you been deep into a long Claude Code session - deeply accumulated context, tools running, real progress - when suddenly every message fails with a cryptic
thinking blocks cannot be modifiederror? - Does retrying do nothing, restarting Claude Code does nothing, and the session is effectively bricked with no clear way to recover?
- Is the error message pointing you at
messages.N.content.Mbut you have no idea what that path means or what you're supposed to do with it? - Have you already lost hours worth of session context because you couldn't figure out how to fix the corrupted history and had to start over?
- Are you staring at a
.jsonlfile with thousands of lines wondering which ones are broken and how to fix them without destroying the rest of your conversation?
This guide explains how to recover the session first, then explains why it happens.
- Installation
- Quick Fix (Recommended)
- If Quick Fix Fails
- Safety
- Compatibility
- The Error
- Why This Happens (Root Cause)
- Deep Dive (Optional)
- Prevention
# Clone the repo
git clone https://github.com/user/claude-code-thinking-blocks-fix.git
cd claude-code-thinking-blocks-fix
# Optionally, add it to your PATH
cp claude-session-fix-thinking ~/.local/bin/# 1) Diagnose a broken session
claude-session-fix-thinking diagnose SESSION_ID
# 2) Apply targeted fix (auto-creates backup)
claude-session-fix-thinking fix SESSION_ID
# 3) Resume your session
claude --resume SESSION_ID- Symptom: Same error after
fix- Run:
claude-session-fix-thinking diagnose SESSION_ID
claude-session-fix-thinking nuke SESSION_ID
claude --resume SESSION_ID- Expected: session resumes without the
messages.N.content.Mthinking-block error. - Symptom:
SESSION_ID.jsonlnot found- Run:
ls ~/.claude/projects/*/SESSION_ID.jsonl- Expected: you find the exact project path for your session file.
- Symptom: Debug log file missing
- Action: continue anyway;
diagnoseworks directly from JSONL.
- Action: continue anyway;
- Symptom: Invalid JSON lines / parse errors
- Action: restore the latest backup file, then rerun
fix.
- Action: restore the latest backup file, then rerun
- Symptom: Still unrecoverable after
nuke- Last resort: salvage visible conversation into a recovery note, then start a fresh session with that note as seed context.
- Example salvage command:
grep -E '"type":"(user|assistant)"' SESSION_ID.jsonl > SESSION_ID.salvage.jsonlfixandnukeautomatically create a backup before writing changes.- Backup location/pattern: same directory as session JSONL,
<session-id>.jsonl.<unix_ts>.backup. - If you manually edit session files, create your own backup first:
cp SESSION.jsonl SESSION.jsonl.$(date +%s).backupfixis targeted recovery: it edits detected thinking-block corruption patterns and trims trailing API error/progress/system tail noise.nukeremoves allthinking/redacted_thinkingblocks from assistant messages but keeps user/assistant text and tool traces.- Data loss boundary: missing
redacted_thinkingpayloads cannot be reconstructed.
- Verified with Claude Code
2.1.42(at time of writing). - Expected to work when session JSONL/debug format matches current Claude Code structure.
- Known unsupported or partial-support cases:
- Session files with invalid JSON lines (for example truncation or partial writes).
- Corruption modes unrelated to thinking-block integrity (for example broken tool-call schema/data).
- Histories where required
redacted_thinkingbytes were never persisted and cannot be inferred. - Major future Claude Code format changes that alter event/message serialization shape.
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.
This is an API validation error where the Anthropic API rejects a request because thinking/redacted_thinking content blocks in the conversation history have been corrupted - either modified, reordered, or orphaned from their original response.
TL;DR: Interleaved assistant chunks from different messages, combined with repair logic that mutates/reorders thinking blocks and missing persisted redacted_thinking data, causes signature mismatches that Anthropic rejects.
ELI5: We accidentally mixed parts of two answers, changed their order, and lost a few pieces, so the API says, "This is not the exact original answer."
Claude Code stores session history as JSONL files where each streaming chunk is a separate line. Two bugs interact to cause this:
During long sessions, streaming responses from parallel or rapidly sequential API calls can interleave in the JSONL. Two different msg_ids get their chunks written adjacently:
line 100: assistant id=msg_AAA -> text("\n\n")
line 101: assistant id=msg_AAA -> thinking(765ch, sig=Epg...) <- host
line 102: assistant id=msg_BBB -> thinking(1371ch, sig=Euo...) <- INTRUDER
line 103: assistant id=msg_AAA -> text("The answer is...") <- host resumes
line 104: assistant id=msg_BBB -> text("Let me check...")
line 105: assistant id=msg_BBB -> tool_use(Bash)
When Claude Code reconstructs the API messages array, it merges consecutive assistant lines into one message. The merged message ends up with two thinking blocks from different API responses with different cryptographic signatures.
Claude Code's ensureToolResultPairing function detects and repairs missing tool_result messages. During repair, it can:
- Split merged assistant messages by inserting bare
usermessages - Reconstruct content arrays, mutating
redacted_thinkingblocks - Reorder content blocks within a message
The API validates thinking blocks byte-for-byte against their cryptographic signatures. Any mutation breaks the signature.
The JSONL writer does not persist redacted_thinking blocks (opaque encrypted blobs). These exist only in memory. When a session crashes or ensureToolResultPairing reconstructs messages, the redacted_thinking blocks are lost or corrupted. The API then rejects the request because the thinking block's signature covers content that's no longer present.
# Session JSONL (the conversation history)
ls ~/.claude/projects/*/SESSION_ID.jsonl
# Debug log (detailed error traces)
ls ~/.claude/debug/SESSION_ID.txt# Check debug log for the specific error
grep "thinking.*blocks.*cannot be modified" ~/.claude/debug/SESSION_ID.txt
# Note the message index: "messages.N.content.M"
# Check for ensureToolResultPairing repairs
grep "ensureToolResultPairing" ~/.claude/debug/SESSION_ID.txtThe key pattern: consecutive assistant JSONL lines with different msg_ids where at least one has a thinking block.
# Use the automated script
claude-session-fix-thinking diagnose SESSION_IDOptional (advanced): inspect with Python:
import json
with open("SESSION_ID.jsonl") as f:
lines = f.readlines()
prev_type, prev_id, prev_line, prev_thinking = None, None, None, False
for i, line in enumerate(lines):
obj = json.loads(line)
t = obj.get('type', '')
if t != 'assistant':
prev_type = t
continue
msg = obj.get('message', {})
msg_id = msg.get('id', '')
content = msg.get('content', [])
has_thinking = any(
isinstance(b, dict) and b.get('type') in ('thinking', 'redacted_thinking')
for b in (content if isinstance(content, list) else [])
)
if prev_type == 'assistant' and msg_id != prev_id and (has_thinking or prev_thinking):
print(f"CORRUPTION: lines {prev_line}-{i}, ids {prev_id[:20]}->{msg_id[:20]}")
prev_type, prev_id, prev_line, prev_thinking = t, msg_id, i, has_thinkingfix auto-creates a backup before writing; see Safety for details.
line N: assistant id=AAA -> thinking(...) <- HOST
line N+1: assistant id=BBB -> thinking(...) <- INTRUDER
line N+2: assistant id=AAA -> text(...) <- HOST continues
Fix: Merge intruder's thinking text into host's thinking block, keep host's signature, delete the intruder line.
host = json.loads(lines[N])
intruder = json.loads(lines[N+1])
# Merge thinking text
host_thinking = host['message']['content'][0]['thinking']
intruder_thinking = intruder['message']['content'][0]['thinking']
host['message']['content'][0]['thinking'] = host_thinking + "\n\n" + intruder_thinking
# Signature stays as-is from host
lines[N] = json.dumps(host, ensure_ascii=False) + "\n"
del lines[N+1]line N: assistant id=AAA -> text(...)
line N+1: assistant id=BBB -> thinking(...) <- ORPHANED
line N+2: assistant id=BBB -> text(...)
Fix: Remove the thinking-only line. The text blocks carry the conversation; the thinking block is just internal reasoning.
del lines[N+1] # Remove the orphaned thinking lineWhen the session is stuck on this error, the tail of the JSONL accumulates API error entries and retry noise. Strip them:
# Walk backwards from end, remove error/progress/system lines
# Stop at the last real user or assistant messageIf the targeted fix doesn't work (e.g., the API still rejects due to signature mismatches), strip ALL thinking blocks from the JSONL:
claude-session-fix-thinking nuke SESSION_IDThis removes every thinking content block from every assistant message. The conversation history is fully preserved (text, tool_use, tool_result all remain). You lose the model's internal reasoning traces but gain a working session.
- Avoid very long sessions. The longer a session runs, the more likely streaming interleaving and message array degradation become.
- Watch for streaming fallback warnings in the debug log:
"Stream completed with message_start but no content blocks completed". - If you see
ensureToolResultPairing: repairedin the debug log, the session is already degraded. Consider starting fresh or applying the fix proactively.