Back to Insights
21 Jun 20265 min readAI & LLMs

Your Claude Code Session Is Permanently Stuck on a 400. Don't /clear — Do This.

You hit Esc, you typed "continue," and Claude Code answered with this:

API Error: 400 … 'thinking'/'redacted_thinking' blocks in the latest assistant
message cannot be modified. These blocks must remain as they were in the
original response.

So you try again. Same error. You wait and retry. Same error. The session is now a brick — every single message reproduces the exact same 400, and nothing you type from inside the session can get past it. The official advice is /rewind (which re-runs whatever happened since the checkpoint) or /clear (which throws the conversation away). Both work. Both are also the wrong tool if the thing in that context window is three hours of hard-won state you don't want to rebuild.

There's a third option that keeps everything. It's a two-line surgical edit of one file. But first you have to understand why the session is stuck, because the reason tells you exactly what to cut.

A real, named failure mode

This isn't a fluke. It's one of the most-reported classes of bug on the anthropics/claude-code tracker:

  • #63147: "Resuming an extended-thinking session fails permanently with 400 'thinking blocks cannot be modified'" — and the title names the mechanism: the transcript stores the thinking text as empty but keeps the signature.
  • #64003: the same 400, specifically after upgrading to Opus 4.8.
  • #12225, #10199, #12130: the same immutable-thinking-block 400 across earlier versions and multi-turn conversations.

Different versions, different triggers, one shape. A fix landed for one variant in v2.1.156, but the interrupted-stream variant still reproduces, which is why this keeps coming back.

The contract that's being violated

Extended thinking has a rule that's easy to miss: once the API sends you a thinking (or redacted_thinking) block, that block is immutable. It comes back with a cryptographic signature, and on every subsequent turn you must send it back byte-for-byte as you received it. The signature is how the server verifies you didn't tamper with the model's reasoning. Change so much as one character — or send a block whose text no longer matches its signature — and the server rejects the whole request with a 400. That's by design; it's an integrity check, not a bug.

Now layer on how Claude Code stores a session. The transcript is a .jsonl file — one JSON object per line — at ~/.claude/projects/<slug>/<session-id>.jsonl, where <slug> is your working directory with the slashes turned into dashes. An assistant turn that used thinking is persisted as a line whose message.content is a list of blocks, and the thinking block looks like this:

{"type":"thinking","thinking":"","signature":"…long-signature…"}

Notice the thinking text is empty but the signature is kept. We pulled that straight out of a live transcript on disk — it's exactly what #63147 describes. For a thinking block sitting in the middle of a conversation, this is harmless: Claude Code knows to drop it when rebuilding the request. The trouble is when an orphaned, empty-text, still-signed thinking block is the last assistant message in the file.

Why it gets orphaned — and why it's permanent

The block ends up stranded when an extended-thinking turn is cut mid-stream: the model started thinking, the stream got interrupted before it produced text or a tool call, and the partial turn was written to the transcript anyway. A common way to trigger it: running an interactive/TUI CLI raw inside a Bash tool call, where the subprocess garbles the stream mid-turn.

Now the transcript ends on an assistant message that is only a signed-but-empty thinking block. Every time you continue, Claude Code rebuilds the request from the transcript, faithfully replays that last assistant message — including the thinking block whose text no longer matches its signature — and the server rejects it. The block is resent on every request, so the failure is total and self-perpetuating. The session can't fix itself, because the poison is the last thing it always re-reads.

The fix: cut the orphan, keep the context

If the problem is one bad trailing turn, the fix is to remove exactly that turn so the file ends at the last clean boundary — the previous user line (typically a tool_result). Then resume. Everything before the cut is untouched, so you lose nothing but the interrupted half-turn.

One critical ordering rule: quit the stuck session first. A live session holds the conversation in memory and rewrites the transcript on exit — so if you edit the file while the session is open, your repair gets clobbered the moment the session flushes. Quit, edit, then claude --resume.

We wrapped the whole thing in a 150-line, zero-dependency tool, unwedge, so you don't have to hand-edit JSON under stress:

unwedge find                    # the newest transcript .jsonl for this directory
unwedge diagnose <transcript>   # is it the thinking-block wedge?  (exit 1 if yes)
unwedge fix <transcript>        # DRY-RUN: shows the exact cut it would make
unwedge fix <transcript> --write   # backs up, then truncates to the last clean user line

diagnose only fires on the real signature — the last message is an assistant turn whose content ends in a thinking block, with no user turn after it. A turn that ends in a tool_use (a normal mid-tool pause) is not flagged; a healthy transcript is reported clean and fix refuses to touch it. fix defaults to a dry-run, writes a timestamped .unwedge-bak-… backup before changing anything, and lands the file on the last user line so the resume starts from a valid state.

It's test-first — 12/12 green across the orphan case, the healthy-transcript no-op, the trailing-tool_use false-positive guard, idempotency, and the backup. We also ran it against a real on-disk transcript: it correctly calls a live, healthy session clean (no false positive), and a real-shaped wedged copy round-trips diagnose → fix → clean.

The real lesson

The tool is the small part. The lesson is the boundary, again: a signed thinking block is immutable, and an interrupted stream is what strands one as your last turn. Two takeaways fall out of that. First, don't run interactive/TUI CLIs raw inside an agent's shell — force non-interactive output (TERM=dumb tool --json | cat) so a subprocess can't garble a turn mid-stream. Second, when a session does wedge, you don't have to choose between losing your place (/rewind) and losing the whole conversation (/clear). The state is sitting in a file on disk, intact except for one trailing line. Cut the line, keep the work.

We build and run autonomous agent fleets on Claude Code in production. If you're hardening a multi-agent setup, talk to us.


Get the tool → unwedge in agent-ops — open source (MIT), a single dependency-free script, part of our toolkit for running coding agents in production.

AI agentsClaude Codedebuggingrecovery

Found this useful?