I keep a small loop that drafts App Store review replies, written directly against the Messages API. It works through years of reviews, so the conversation gets long, and my system prompt rebuilt a line like "Today is 2026-09-02" on every request. Tone shifts with the date, so putting it there felt obvious at the time.
That "rebuild it every request" habit started to matter on September 1. On Claude Fable 5.1, a request that changes anything before a thinking block now fails a check. My loop was firmly on the failing side.
Before you go rewrite everything: the check applies to Fable 5.1 only, and only to accounts created on or after August 31, 2026, 00:00 UTC. Claude Code, Claude Cowork, claude.ai, and Claude reached through a third-party product are all unaffected. Anthropic has said it will apply to every account on future models, though, so auditing your loop now is not wasted work.
The rejection happens when something before the thinking block changed
Claude produces reasoning before its answer, and the API returns it as a thinking block. In a multi-turn conversation you send those blocks back, signature included. The API now uses that signature to verify that the same system prompt, tools, and messages that produced the block are the ones arriving with it. If they differ, you get a 400.
messages.1.content.0: Invalid `signature` in `thinking` block. The block is bound to a
different conversation. Remove the block, or set
`thinking.block_binding.prefix_mismatch_behavior` to "drop_block".
Three things get checked: the model is the same or newer, nothing before the block has changed in system, tools, or messages, and the chain of earlier thinking blocks is unbroken. That third one is easy to miss. Dropping thinking blocks from the front of the history is fine. Pulling one out of the middle invalidates every thinking block after it.
Anthropic publishes the full table of what counts as an edit. These are the rows I found myself checking against real code.
| Change between two consecutive requests | Later thinking blocks |
|---|---|
| Append messages at the end | Valid |
Change a parameter outside system, tools, and messages (max_tokens, tool_choice, and so on) | Valid |
Add, move, or remove a cache_control marker | Valid |
| Server-side compaction or context editing removes content | Valid — the check compares what you sent |
| Edit, reorder, or delete an earlier user / assistant / system message | Invalid |
Change the top-level system string or blocks | Invalid |
Add, remove, rename, or edit a tool in tools | Invalid |
| Remove a thinking block from the middle of the history | Invalid for every later block |
| An image or document URL that returns different bytes next request | Invalid |
The last row caught me off guard. If you reference a "latest screenshot" endpoint every turn, the URL string stays the same while the bytes change, and the thinking after it stops verifying. For anything you carry across turns, upload it once and reference the file_id, or send base64.
One request tells you whether your loop is affected
Rather than guess, run the check. Send the beta header thinking-binding-controls-2026-08-01 and set prefix_mismatch_behavior to "drop_block", and enforcement turns on regardless of how old your account is. From an older key, you get to see what a user on a new account would see.
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: thinking-binding-controls-2026-08-01" \
-d '{
"model": "claude-fable-5-1",
"max_tokens": 16000,
"thinking": {
"type": "adaptive",
"block_binding": { "prefix_mismatch_behavior": "drop_block" }
},
"system": "You are a review-reply assistant.",
"messages": [
{ "role": "user", "content": "Draft a reply to this one-star review." },
{
"role": "assistant",
"content": [
{ "type": "thinking", "thinking": "", "signature": "EqQBCkYIBxgCKkD..." },
{ "type": "text", "text": "Could you paste the review text?" }
]
},
{ "role": "user", "content": "\"I paid and the ads are still there.\"" }
]
}'Run a few normal turns that way and every response carries a top-level input_transformations array. Logging it each turn is enough to tell you whether your loop is append-only.
{
"input_transformations": [
{
"type": "thinking_dropped",
"path": "messages.1.content.0",
"reason": "prefix_binding_mismatch"
}
]
}There are two readings. prefix_binding_mismatch means something before the block at path changed since the last request — keep the previous request body around, diff system, tools, and the shared part of messages, and the culprit surfaces immediately. model_binding_mismatch means the conversation moved to a model that cannot read the earlier blocks, usually through a router or a fallback. That one is not a bug in your code; keep sending the blocks and let the API drop what the current model cannot read.
What I ended up with is small: I hold one previous request body in memory and compare system and tools right before sending the next one. In CI I set "error" instead, so a history edit turns the build red.
What I had to change was the rebuilt system prompt
Injecting the date on every request turned out to be a clean swap for a mid-conversation system message. Freeze the top-level system at session start, and when something new becomes true, append a role: "system" message inside messages. Nothing before it moves by a single byte.
# Before: system rebuilt on every request, invalidating every later thinking block
system = f"You are a review-reply assistant. Today is {date.today()}."
# After: system is fixed for the session, and changes are appended to messages
system = "You are a review-reply assistant."
messages.append({
"role": "system",
"content": "The date has changed. Today is 2026-09-02. Use this for any dates you mention.",
})
messages.append({"role": "user", "content": next_review_text})On Fable 5.1 this is stable and needs no beta header, and the model treats it with system-prompt authority. Inside a tool loop, place it after the user message carrying the tool_result, never between an assistant tool_use and its tool_result.
Tool changes follow the same shape. Instead of editing the tools array, declare the full set at session start and use tool_addition and tool_removal to offer or withdraw a tool from that point on. I wrote about how a one-line tool edit throws away the cached prefix in designing a tool registry for mid-conversation swaps; with this check in place, the consequence is no longer just a cold cache.
Trimming history is allowed — the shape decides whether it passes
The other common edit is trimming a conversation that grew too long. This is not banned outright. The rule is narrower: never leave a thinking block behind a prefix you rewrote.
| Compaction shape | Passes the check? | What it needs |
|---|---|---|
| Server-side compaction / context editing | Yes | Nothing — the check compares what you sent |
| Simple compaction (one summary, start the next request fresh) | Yes | Nothing. No earlier thinking is carried across |
| Keep-tail compaction (summarize old turns, keep recent ones verbatim) | No | Strip thinking and redacted_thinking from carried-over assistant turns, keeping text and tool_use |
| Background compaction (build the summary off the critical path, swap it in) | No | Send "drop_block" on requests still carrying pre-swap thinking, or compact synchronously |
| Snipping individual turns out of the middle | No | No client-side shape avoids this. Use a mid-conversation system message or server-side context editing |
What surprised me is that simple compaction is the recommended shape. Rebuild messages from one summary plus the next instruction, replay no earlier turns and no thinking, and the model reasons afresh over the compacted conversation — Anthropic says it performs comparably to more elaborate schemes for most workloads. I have built the layered kind before, described in hierarchical summarization of chat history, and weighed against the upkeep, moving to the simple shape is a defensible trade.
One ordering rule holds regardless: never compact in the middle of a tool round. An assistant turn whose tool_use is still waiting on its tool_result should go back with its thinking intact, so the model can finish the round with the reasoning it started.
Choosing between error and drop_block in production
"error" is the default, and once your loop is append-only, a mismatch can only mean a bug on your side. Failing loudly is the healthier choice there, and it is what I run in CI.
"drop_block" earns its place when degrading beats failing: a routed or fallback setup, or several clients touching the same conversation. Operations get quieter. The cost is silence, so pair it with logging input_transformations every time. Without that log, a later drop in output quality has no explanation attached.
Decide the recovery path too. Retrying the same request after a 400 will not clear it. Resend with the beta header and "drop_block", and keep sending "drop_block" for the rest of that session. If the beta is not an option, strip every thinking and redacted_thinking block from the history, leave each turn's text and tool_use in place, and retry once. Then go fix the edit itself.
There is a side benefit worth naming. A conversation whose prefix never moves keeps the prompt cache warm, which shows up directly in what extended thinking costs to run. I put my numbers on that in what extended thinking actually cost in production.
I have come to read this less as a restriction than as a design hint. Conversation history is a place to append, not a place to revise. A date change, a mode switch, a tool appearing or disappearing — each can be expressed by adding "from here onward, this is true" rather than reaching backward. Obvious in hindsight, and it took me years of rebuilding system on every request to see it.
Run one session of your own loop under "drop_block" and watch whether input_transformations stays empty. If it does not, path hands you the first line to look at. That is where I started.