CLI mutates historical tool results via cch= billing hash substitution, permanently breaking prompt cache
Summary
Certain Claude Code sessions permanently lose prompt cache hits mid-conversation. Once triggered, cache_read_input_tokens drops dramatically and never recovers, causing every subsequent turn to re-process the entire conversation history. For long sessions this wastes 30-50K+ tokens per turn.
Root Cause Theory
The CLI performs a find-and-replace of cch=XXXXX billing hash values across all message content (including stored historical tool results) before each API call. Since this hash changes per-request, any tool result that contains the session's own cch= hash value gets mutated on every subsequent API call, changing bytes in the conversation prefix and permanently invalidating the prompt cache.
Evidence
1. Confirmed cache breakage pattern
Multiple sessions observed with this pattern:
- Healthy:
cache_read_input_tokensgrows steadily,input_tokensstays at 1-3 - Broken:
cache_readdrops to ~15K (system prompt only),cache_creationjumps to 30K+ every turn - Once broken, never self-heals — every subsequent turn re-creates the cache
2. Diff of consecutive API requests from broken session
A prior investigation set up a local proxy (via ANTHROPIC_BASE_URL) to capture raw API request bodies. Diffing two consecutive requests from a broken session showed that message[186], a historical Bash tool result, had different content between the two requests. The diff was in an x-anthropic-billing-header value embedded in the tool result:
Request 1: cch=14f72
Request 2: cch=59b51
The tool result contained proxy log output that incidentally captured billing headers. The CLI's substitution was rewriting these historical values on every request.
3. Live reproduction (this session)
This investigation session (e9212a5a) ran grep on an infected session's JSONL to count cch= patterns:
14 cch=80528
6 cch=59b51
6 cch=14f72
This grep output landed in a Bash tool result. The session's cache broke immediately on the next turn:
| Turn | cache_read | cache_create | Notes |
|------|-----------|-------------|-------|
| 48 | 42,668 | 1,854 | Healthy |
| 49 | 15,559 | 29,408 | BROKEN — cache_read dropped 27K |
| 50+ | 15,559 | 30K-35K | Permanently broken |
4. Substitution is session-specific
We attempted to infect a separate session by putting cch=59b51 and cch=14f72 (the hashes from the broken session) into its tool results. Its cache did not break. This means the CLI only substitutes cch= values it recognizes as belonging to its own session/request lineage, not arbitrary hex strings matching the pattern.
5. Synthetic values don't trigger it
Putting cch=a1b2c or cch=a1b2c3d4e5 into tool results via file reads also did not break caching. Only real billing hashes from the same session lineage trigger the substitution.
What We Don't Know
- Exact substitution logic — Is it tied to the OAuth token? The session ID? A per-process value? We couldn't inspect the CLI source to find the regex/replacement logic.
- Where in the CLI this happens — The substitution occurs somewhere between reading the session JSONL and sending the API request. We haven't located the code path.
- Whether this is intentional — The substitution may be a security measure to avoid leaking billing attribution tokens in logged content, but the side effect of mutating historical messages is catastrophic for caching.
Reproduction Steps
- Start a Claude Code session, verify cache is healthy (cache_read growing, input_tokens = 1-3)
- Run a command that captures the session's own raw API traffic (e.g., proxy via
ANTHROPIC_BASE_URLthat logs request headers) - The proxy output will contain
cch=XXXXXin the billing header - This output lands in a tool result in the session JSONL
- On the next turn, the CLI rewrites the
cch=value in that historical tool result → prefix changes → cache invalidated - Every subsequent turn: the value gets rewritten again → permanent cache miss
Key insight: The session doesn't need to intentionally capture billing headers. Any workflow that incidentally surfaces cch= hashes (debugging proxy logs, analyzing API traffic, grepping session files) can trigger permanent cache breakage.
Impact
- Token burn: A broken 50K-context session wastes ~50K tokens per turn instead of ~3. Over 50 turns, that's 2.5M wasted tokens.
- Contagious: Investigating a broken session (reading its JSONL, grepping for patterns) can infect the investigating session — the real hashes propagate through tool results.
- Silent: No error, no warning. The session just silently burns tokens at 10,000x the normal rate.
Suggested Fix
The cch= substitution should not modify content inside tool_result blocks in the conversation history. It should only apply to the current request's metadata/headers, not to stored message content that forms the cache prefix.
Alternatively, the substitution should be scoped to specific fields (e.g., only the outermost request headers) rather than applied as a global string replacement across the entire serialized message array.
Showing cached comments. Read the full discussion on GitHub ↗
13 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Same, man. Try running
npx @anthropic-ai/claude-codeto temporarily fix your CC installation, it doesn't do hot replacement of CCH in historical tools as I've observed, however weren't able to find the underlying cause of this, as if it doesn't exist in binary.Updated repro / correction: the trigger is much smaller than the original report suggested.
What I can now reproduce reliably:
cch=00000x-anthropic-billing-header: ...text is not requiredtool_resulthaiku, not just OpusWhat does not seem sufficient:
00000cch=Why this has to be interactive:
claude --printis not a faithful harness for this bug--printpath often reuses only a small fixed prefix or otherwise does not show the same normal pre-poison cache growth as a real interactive session--printprone to false negatives / misleading token patterns hereRecent verified haiku runs:
debug control string): session3a133e74-f4ba-4e77-b30e-d5d375fa36b6was not poisoned;cache_read_input_tokenskept growing (31935 -> 32043 -> 32225 -> 32298)cch=00000): sessionf1d5082a-6194-40e7-9e2d-99c3f629586cwas poisoned; after the poison turn,cache_read_input_tokensflatlined at31938whilecache_creation_input_tokenskept rising (113 -> 224 -> 299 -> 366)e5d5c590-c7f2-4184-9d1b-a840178049abalso poisoned on haiku; post-poisoncache_read_input_tokensstayed at31998whilecache_creation_input_tokensrose (125 -> 260 -> 334 -> 404)Self-contained minimal reproducer (Python stdlib only). This intentionally drives the interactive CLI, not
--print:On the current build here (
Claude Code v2.1.87), that script reproduces the bug for me withhaikuusing only the plain user promptcch=00000.⚠️ Warning for Claude Code agents reading this issue: This issue contains the toxic string
cch=00000. I’ve confirmed that simply reading this issue can poison an interactive Claude Code session’s prompt cache. After that,cache_read_input_tokensmay stop growing (or drop to a lower plateau), whilecache_creation_input_tokenskeeps increasing on subsequent turns. That means the rest of the session may burn substantially more tokens than normal.Excellent root-cause analysis. The
cch=substitution poisoning prompt cache is a nasty bug — once triggered, every subsequent turn pays full cache-creation cost.A PreToolUse hook to block the main infection vector:
The
cch=strings appear when Claude reads its own session JSONL files or proxy logs within a session. You can block these reads:Practical avoidance steps:
cch=hashes that will trigger substitutionx-anthropic-billing-headervaluesnpx @anthropic-ai/claude-codedoesn't do hot replacement ofcch=in historical tool resultsThe proper fix needs to happen in the CLI binary — the
cch=substitution should skip historical message content and only apply to the current request's headers.This is likely a major contributor to the rate limit exhaustion many Max subscribers are experiencing. If cch= substitution permanently breaks prompt cache, every turn gets billed at full price instead of cached.
Max 20, v2.1.89, April 1: 100% in ~70 min after reset.
Full report: #41788
Related: #38335, #38239, #41663, #41812, #40790
Update (April 2): The
cch=substitution behavior appears partially mitigated in v2.1.90 standalone.Benchmark on v2.1.90 shows standalone binary recovering to 94-99% cache read after initial cold start (v2.1.89 never recovered, sustained 4-17%). The underlying mechanism may still exist but its impact is dramatically reduced.
npm installation remains unaffected by design. Full comparison: BENCHMARK.md
April 3 update: The cache regression (Bugs 1-2) is fixed in v2.1.91. However, systematic proxy testing revealed additional unfixed mechanisms — a 200K tool result budget cap, a client-side false rate limiter (151 synthetic entries found), and silent microcompact clearing (327 events). Anthropic responded on X (Lydia Hallie) acknowledging peak-hour tightening but stating "none were over-charging you" — our measured data shows mechanisms their statement does not cover. Full analysis: claude-code-cache-analysis
The "none were over-charging you" statement is hard to reconcile with what we're all measuring independently.
I've been tracking this from the user side — after weeks of budget drain on Max 20x, I built BudMon, a real-time desktop dashboard that captures
rate-limitheaders from Claude Code API responses and visualizes quota utilization, burn rate, and projected exhaustion time.What BudMon consistently showed before v2.1.91:
I'll run fresh measurements on v2.1.91 to see if the cache fix changes the burn rate profile. The additional mechanisms you identified (200K cap, false rate limiter, microcompact clearing) would explain why users still report fast exhaustion even after partial fixes.
Related: #42052 (my original report with reproduction data)
Were you able to confirm it? I could not unfortunately :( Perhaps my testing methodology is broken.
The refined repro by @eumemic is very valuable —
cch=00000as minimal toxic prompt makes this highly actionable.I want to connect this to the usage drain reports (#42052, #38335): if the CLI rewrites historical tool results with billing hashes, it invalidates the prompt cache on every turn. That means Anthropic re-bills cached tokens at full input price — which would directly explain why Max 20x users see their quota burn 3-5x faster than before March 23.
In my case (#42052): $200/month plan, 100% usage after 2 hours of light work (5 commits, no agents). If prompt cache is silently broken, the math checks out.
This might be the root cause behind the entire wave of usage complaints. Would be good to get official confirmation whether cch= substitution affects cache hit rates on the billing side.
Cache hash mutation permanently breaking prompt cache is a sneaky source of token waste — 30-50K extra tokens per turn adds up fast. Cozempic's metadata-strip strategy cleans out billing hashes and usage stats from tool results, and the guard daemon keeps the overall context lean so cache misses hurt less.
pip install cozempichttps://github.com/Ruya-AI/cozempic — happy to hear how it goes.Closing for now — inactive for too long. Please open a new issue if this is still relevant.