[BUG] Conversation history invalidated on subsequent turns

Resolved 💬 124 comments Opened Mar 29, 2026 by jmarianski Closed Apr 4, 2026
💡 Likely answer: A maintainer (dltn, contributor) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

While investigating huge token usage I've noticed it come due to fact suddenly my conversation history gets invalidated and all subsequent turns revert to only caching system prompt and huge cache writes.

What Should Happen?

Cache shouldn't drop due to history changes. History should not be updated. Or we shouldn't be charged for historical updates.

Error Messages/Logs

Analysis of token usage from the start of my analysis:

time        cache_read  cache_cr    input    out    model               stop        
----------  ----------  ----------  -------  -----  ------------------  ------------
22:22:48    312377      1944        1        215    opus-4-6            end_turn    
22:23:39    314321      493         3        159    opus-4-6            end_turn    
22:24:19    314814      172         3        108    opus-4-6            end_turn    
22:33:42    0           0           8        1      haiku-4-5-20251001  max_tokens  <-- resume
22:34:26    11428       305735      3        213    opus-4-6            tool_use    <-- irrelevant cache rewrite after restart
22:34:35    317163      579         1        239    opus-4-6            tool_use    
22:34:43    317742      566         1        152    opus-4-6            end_turn    
22:37:13    318308      245         3        96     opus-4-6            end_turn    
07:55:55    0           0           8        1      haiku-4-5-20251001  max_tokens  <-- resume
07:56:22    11428       163547      3        143    opus-4-6            tool_use    <-- partial cache regenerate (wth?)
07:56:40    174975      358         1        90     opus-4-6            end_turn    
07:57:25    11428       307626      3        87     opus-4-6            end_turn    <-- full cache regenerate
07:57:51    319054      108         3        89     opus-4-6            tool_use    
07:58:05    319162      712         1        448    opus-4-6            tool_use    
07:58:21    319874      833         1        367    opus-4-6            end_turn    
07:59:21    320707      393         3        414    opus-4-6            tool_use    
07:59:34    321100      609         1        560    opus-4-6            tool_use    
07:59:47    321709      948         1        512    opus-4-6            tool_use    
08:00:10    322657      615         1        348    opus-4-6            end_turn    
08:03:00    323272      426         3        530    opus-4-6            tool_use    
08:03:12    323698      972         1        468    opus-4-6            tool_use    
08:03:22    324670      529         1        167    opus-4-6            end_turn    
08:03:29    325199      215         3        28     opus-4-6            end_turn    
08:05:30    0           0           8        1      haiku-4-5-20251001  max_tokens  <-- resume
08:05:48    11428       187695      3        155    opus-4-6            tool_use    
08:06:06    199123      876         1        780    opus-4-6            tool_use    
08:06:25    199999      2199        1        1285   opus-4-6            tool_use    
08:06:38    202198      1633        1        302    opus-4-6            end_turn    
08:08:06    203831      481         3        88     opus-4-6            tool_use    
08:08:17    204312      408         1        175    opus-4-6            end_turn    
08:09:16    204720      206         3        154    opus-4-6            end_turn    
08:10:25    204926      228         3        503    opus-4-6            tool_use    
08:10:34    205154      1193        1        507    opus-4-6            tool_use    
08:10:45    206347      1007        1        247    opus-4-6            end_turn    
08:10:54    0           0           8        1      haiku-4-5-20251001  max_tokens  <-- resume
08:11:16    11428       195983      3        136    opus-4-6            tool_use    
08:11:37    207411      616         1        1207   opus-4-6            tool_use    
08:11:49    208027      1457        1        290    opus-4-6            end_turn    
08:12:02    209484      323         3        270    opus-4-6            end_turn    
08:12:27    209807      284         3        190    opus-4-6            tool_use    
08:12:39    210091      314         1        278    opus-4-6            tool_use    
08:13:01    210405      728         1        1219   opus-4-6            tool_use    
08:13:18    211133      1465        1        449    opus-4-6            end_turn    
08:15:28    212598      599         3        325    opus-4-6            end_turn    
08:16:26    213197      334         3        209    opus-4-6            end_turn    
08:18:00    213531      288         3        137    opus-4-6            tool_use    
08:18:07    213819      1131        1        122    opus-4-6            tool_use    
08:18:15    214950      140         1        193    opus-4-6            tool_use    
08:18:29    215090      1114        1        269    opus-4-6            tool_use    
08:18:54    216204      10504       1        336    opus-4-6            tool_use    <-- cache starts breaking down due to history change*
08:19:07    216204      11815       1        228    opus-4-6            tool_use    
08:19:17    216204      12990       1        134    opus-4-6            tool_use    
08:19:38    216204      13341       1        301    opus-4-6            tool_use    
08:20:04    216204      13758       1        426    opus-4-6            tool_use    
08:20:18    216204      15278       1        154    opus-4-6            tool_use    
08:20:46    216204      15778       1        508    opus-4-6            tool_use    
08:22:25    216204      17092       1        208    opus-4-6            tool_use    
08:22:51    216204      17894       1        660    opus-4-6            tool_use    
08:23:22    11428       224502      1        315    opus-4-6            end_turn    <-- cache cannot get regenerated, reverting to full cache write
08:24:47    11428       224953      3        871    opus-4-6            tool_use    
08:25:10    11428       227259      1        597    opus-4-6            tool_use    
08:25:24    11428       228249      1        356    opus-4-6            tool_use    
08:25:43    11428       228669      1        825    opus-4-6            tool_use    
08:26:01    11428       229763      1        468    opus-4-6            tool_use    
08:26:22    11428       230278      1        339    opus-4-6            end_turn    
08:28:07    11428       230642      3        442    opus-4-6            end_turn    
08:37:30    11428       231432      3        430    opus-4-6            end_turn    

---
(Ignore hour, it's another day)
When running "npx @anthropic-ai/claude-code"
21:28:59    11374       46622       1        473    opus-4-6            tool_use    <-- still on standalone binary
22:02:13    0           0           8        1      haiku-4-5-20251001  max_tokens  <-- I tried resuming a couple of times
22:02:23    0           0           340      11     haiku-4-5-20251001  end_turn    
22:02:25    11374       15278       3        21     opus-4-6            end_turn    
22:04:51    0           0           8        1      haiku-4-5-20251001  max_tokens  
22:04:58    0           0           341      11     haiku-4-5-20251001  end_turn    
22:05:00    11374       15194       3        20     opus-4-6            end_turn    
22:09:20    0           0           8        1      haiku-4-5-20251001  max_tokens  
22:09:35    0           0           8        1      haiku-4-5-20251001  max_tokens  
22:12:16    0           0           341      11     haiku-4-5-20251001  end_turn    
22:12:18    11374       15194       3        21     opus-4-6            end_turn    
22:15:36    0           0           8        1      haiku-4-5-20251001  max_tokens  
23:22:46    0           0           8        1      haiku-4-5-20251001  max_tokens  
23:23:06    0           0           341      12     haiku-4-5-20251001  end_turn    
23:23:09    11374       17262       3        19     opus-4-6            end_turn    
23:23:26    28636       27          3        12     opus-4-6            end_turn    
23:31:41    0           0           8        1      haiku-4-5-20251001  max_tokens  
23:31:50    0           0           345      13     haiku-4-5-20251001  end_turn    
23:31:54    11374       17188       3        32     opus-4-6            end_turn      <-- start of npx trials
23:32:25    28562       51          3        167    opus-4-6            end_turn    
23:33:52    28613       320         3        666    opus-4-6            end_turn    
23:34:55    0           0           8        1      haiku-4-5-20251001  max_tokens  
23:35:12    0           0           355      15     haiku-4-5-20251001  end_turn  
23:35:22    11374       17198       3        328    opus-4-6            end_turn    
23:36:50    28572       367         3        500    opus-4-6            end_turn    
23:37:15    28939       506         3        143    opus-4-6            tool_use    
23:37:19    29445       523         1        91     opus-4-6            tool_use    
23:37:56    29968       4869        1        1284   opus-4-6            tool_use    
23:38:06    34837       1343        1        173    opus-4-6            end_turn    
23:38:19    36180       219         3        151    opus-4-6            tool_use    
23:38:27    36399       9511        1        341    opus-4-6            tool_use    
23:38:33    45910       442         73       77     opus-4-6            tool_use    
23:38:37    46352       250         1        77     opus-4-6            tool_use    
23:38:42    46602       1161        1        134    opus-4-6            tool_use    
23:38:59    47763       415         1        369    opus-4-6            tool_use    
23:39:06    48178       427         1        96     opus-4-6            tool_use    
23:39:09    48605       393         1        77     opus-4-6            tool_use    
23:39:13    48605       639         1        152    opus-4-6            tool_use    
23:40:17    11374       38207       3        362    opus-4-6            end_turn    
23:41:35    49581       438         3        766    opus-4-6            end_turn    
23:43:02    0           0           8        1      haiku-4-5-20251001  max_tokens  <-- another session
23:43:23    11374       40201       3        97     opus-4-6            tool_use    
23:43:30    51575       122         1        310    opus-4-6            tool_use    
23:43:35    51697       408         1        152    opus-4-6            tool_use    
23:43:41    52105       219         93       170    opus-4-6            tool_use    
23:43:49    52324       442         1        259    opus-4-6            tool_use    
23:43:54    52766       558         1        102    opus-4-6            tool_use    
23:44:08    53324       2593        1        403    opus-4-6            end_turn    
23:51:33    0           0           8        1      haiku-4-5-20251001  max_tokens  
23:52:30    55917       431         3        187    opus-4-6            tool_use    
23:54:20    56348       292         37       284    opus-4-6            tool_use    
23:54:29    56640       612         158      492    opus-4-6            tool_use    
23:54:54    60657       13          3        508    opus-4-6            end_turn    
23:58:58    60670       717         2        454    opus-4-6            tool_use    
23:59:05    61387       847         1        336    opus-4-6            tool_use    
23:59:23    62234       1282        1        674    opus-4-6            tool_use    
23:59:34    63516       1024        1        506    opus-4-6            tool_use    
23:59:45    64540       583         1        264    opus-4-6            tool_use    
23:59:53    65123       284         1        393    opus-4-6            tool_use    
00:00:10    65407       470         1        887    opus-4-6            tool_use    
00:03:07    65877       1024        1        871    opus-4-6            tool_use    
00:03:16    66901       2098        1        538    opus-4-6            tool_use    
00:03:25    68999       1492        1        379    opus-4-6            tool_use    
00:03:36    70491       1043        1        640    opus-4-6            tool_use    
00:03:43    71534       704         1        233    opus-4-6            tool_use    
00:03:51    72238       250         1        148    opus-4-6            tool_use    
00:03:58    72488       355         1        249    opus-4-6            tool_use    
00:04:03    72843       396         1        259    opus-4-6            tool_use    
00:04:10    73239       435         1        278    opus-4-6            tool_use    
00:04:31    73674       359         1        941    opus-4-6            tool_use    
00:04:45    74033       1595        1        662    opus-4-6            tool_use    
00:05:00    75628       914         1        830    opus-4-6            tool_use    
00:05:18    76542       1610        1        963    opus-4-6            tool_use    
00:05:31    78152       1379        1        640    opus-4-6            tool_use    
00:05:41    79531       1374        1        549    opus-4-6            tool_use    
00:05:51    80905       1576        1        550    opus-4-6            tool_use    
00:06:06    82481       629         1        986    opus-4-6            tool_use    
00:06:20    83110       1102        1        994    opus-4-6            tool_use    
00:06:30    84212       1074        1        578    opus-4-6            tool_use    
00:06:48    85286       2418        1        854    opus-4-6            tool_use    
00:07:01    87704       880         1        555    opus-4-6            tool_use    
00:07:13    88584       818         1        601    opus-4-6            tool_use    
00:07:30    89402       2165        1        520    opus-4-6            end_turn    
00:11:02    91567       542         3        691    opus-4-6            tool_use    
00:11:12    92109       777         1        733    opus-4-6            tool_use    
00:11:25    92886       1042        1        578    opus-4-6            tool_use    
00:11:39    93928       3347        1        694    opus-4-6            tool_use    
00:12:21    98211       39          3        442    opus-4-6            end_turn    
00:13:24    98250       462         3        161    opus-4-6            tool_use    
00:13:32    98712       274         1        178    opus-4-6            end_turn    
00:15:06    98986       190         3        237    opus-4-6            tool_use    
00:15:37    99176       362         1        1202   opus-4-6            tool_use    
00:15:42    6637        16169       2        114    opus-4-6            tool_use    
00:15:44    99538       1427        1        280    opus-4-6            tool_use    
00:15:47    22806       3503        1        160    opus-4-6            tool_use    
00:15:49    100965      2929        1        52     opus-4-6            end_turn    <-- my joy is great at this point
00:15:50    26309       191         1        91     opus-4-6            tool_use    
00:15:54    26500       217         1        92     opus-4-6            tool_use    
00:15:57    26717       152         1        88     opus-4-6            tool_use    
00:16:02    26869       607         3        125    opus-4-6            tool_use    
00:16:06    27476       146         1        166    opus-4-6            tool_use    
00:16:09    27622       430         1        105    opus-4-6            tool_use    
00:16:14    28052       123         1        109    opus-4-6            tool_use    
00:16:19    28052       542         1        176    opus-4-6            tool_use    
00:16:23    28594       440         1        95     opus-4-6            tool_use    
00:16:28    29034       120         1        112    opus-4-6            tool_use    
00:16:32    29154       139         1        180    opus-4-6            tool_use    
00:16:37    29293       256         1        206    opus-4-6            tool_use    
00:17:09    29549       1065        1        82     opus-4-6            tool_use    
00:17:12    30614       131         1        117    opus-4-6            tool_use    
00:17:24    30745       135         1        79     opus-4-6            tool_use    
00:17:28    30745       237         1        100    opus-4-6            tool_use    
00:17:35    30982       140         1        138    opus-4-6            tool_use    
00:17:39    31122       222         1        112    opus-4-6            tool_use    
00:17:46    31344       226         1        257    opus-4-6            tool_use    
00:17:51    31570       288         1        125    opus-4-6            tool_use    
00:17:56    31858       214         1        128    opus-4-6            tool_use    
00:18:01    32072       316         1        139    opus-4-6            tool_use    
00:18:04    32388       487         1        116    opus-4-6            tool_use    
00:18:09    32875       134         1        125    opus-4-6            tool_use    
00:19:29    33009       143         1        5925   opus-4-6            tool_use    
00:19:35    33152       5912        1        113    opus-4-6            tool_use    <-- yup, was at 100% usage at this point
00:19:36    0           0           0        0      -                   -           
00:19:36    0           0           0        0      -                   -           
10:08:54    11374       58355       3        270    opus-4-6            tool_use    <-- costly resume, but cache TTL = 1h in claude code
10:09:00    69729       538         1        277    opus-4-6            tool_use    
10:09:07    70267       7086        180      188    opus-4-6            tool_use    
10:09:14    77353       436         1        201    opus-4-6            tool_use    
10:09:18    77789       219         1        118    opus-4-6            tool_use    
10:09:34    78008       518         1        183    opus-4-6            tool_use    
10:10:40    78526       444         1        95     opus-4-6            tool_use    
10:10:46    78970       1213        1        185    opus-4-6            tool_use    
10:10:55    80183       996         1        270    opus-4-6            tool_use    
10:13:03    81179       602         1        268    opus-4-6            tool_use    
10:18:08    81781       675         1        121    opus-4-6            tool_use    
10:18:14    82456       148         1        226    opus-4-6            tool_use    
10:29:13    82604       823         1        184    opus-4-6            tool_use    
10:29:19    83427       889         1        239    opus-4-6            tool_use    
----------  ----------  ----------  -------  -----  ------------------  ------------


If you provide me with means, I can send you full request/response dumps

*- no idea if this cache breaking was due to me inspecting binary or some historical tool change happened on the background level.

Steps to Reproduce

Write "cch=00000" in command line and ask claude what does he see. He still should see "cch=00000". And token usage should be all "cache read" mostly, not "cache write" for subsequent requests.

Step to temporarily fix: npx @anthropic-ai/claude-code@2.1.34 // you need to fix it on older version to benefit from it

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

Based on reports: 2.1.67

Claude Code Version

2.1.86 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Similar issue: https://github.com/anthropics/claude-code/issues/34629 - this one relates to immediate start of conversation

Tool I wrote for debugging: https://gitlab.com/treetank/cc-diag

Verification script: https://gitlab.com/treetank/cc-diag/-/raw/c126a7890f2ee12f76d91bfb1cc92612ae95284e/test_cache.py

View original on GitHub ↗

124 Comments

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/34629
  2. https://github.com/anthropics/claude-code/issues/27048
  3. https://github.com/anthropics/claude-code/issues/40319

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

jmarianski · 3 months ago

This is not a duplicate. It doesn't relate to session resume, but a subsequent turn invalidation.

RebelSyntax · 3 months ago

im collecting the same evidence. something breaks the cache and it never recovers. model switches mid session is an easy one to pinpoint. the billing header seems to be a proxy for whatever it is. possibly the dynamic tool loading or some sort of memory call back? not sure but the only way to recover seems to be starting a completely brand new session. otherwise cache writes grow unbounded and quickly devour premium tokens.

jmarianski · 3 months ago

"model switches mid session" is one of the behaviors for saving compute done by anthropic, as stated in the email from the robot: "The 20% threshold you're seeing is actually by design for Max 5x plans. Claude Code automatically switches from Opus 4 to Sonnet 4 when you reach 20% of your rate limit (Max 20x users get 50%). This threshold is intentionally set to help preserve a good experience and prevent users from hitting their full rate limits too quickly."

I've had my findings now and it is I've expected: binary holds some code that is revealed after decompiling. Apparently they've secretly injected cch=hash (Zig Wyhash) into the body of a request if a header "anthropic-version" appears in the request. However, if at some point "cch=00000" appears in the body of a request, it will be infected. Reason? No clue. I'm trying to find it out.

It seems like a small side effect of some obscure tool use or some micro compaction that might have already been fixed. I mean I was trying to find out, however code doesn't allow currently for "cch=00000" to accidentally leak.

jmarianski · 3 months ago

Full reverse engineering analysis of the sentinel replacement mechanism

Summary

Through MITM proxy capture + Ghidra reverse engineering of the Claude Code standalone binary (228MB ELF), I've identified the exact mechanism causing conversation history invalidation. The root cause is a native-layer sentinel replacement in Anthropic's custom Bun fork that rewrites the billing attribution header value on every API request.

The Mechanism

Located at VA 0x0374d610 in the standalone binary (v2.1.87) — inside the Zig HTTP header builder function (src/http.zig in Bun's source tree). This is the same function that builds Content-Length, User-Agent, Host, Accept-Encoding, etc.

Trigger condition: The replacement fires when ALL of these are true:

  1. Request contains anthropic-version HTTP header (Wyhash = 0x58e54d60e1462681, verified via Bun.hash())
  2. Request URL path contains /v1/messages
  3. Request body contains the sentinel string cch=00000 (9 bytes)

What it does:

  1. Searches the serialized JSON request body for the first occurrence of cch=00000
  2. Hashes the entire request body (using a hash function with fixed seeds: 0xcf3c9b5975c738f4, 0x310521a7efdb6e6d, 0x6e52736ac806831e, 0xd01af9b9421ab897)
  3. Converts 5 nibbles of the hash to hex characters using SIMD instructions (pshufb/pblendvb)
  4. Writes the 5 hex characters in-place into the body buffer, replacing 00000

Decompiled pseudocode (reconstructed from Ghidra):

// Inside HTTP header builder — after iterating request headers
if (has_anthropic_version_header) {
    // Check URL contains "/v1/messages"
    // Built on stack as u64 LE immediates:
    local_c0 = 0x7373656d2f31762f;  // "/v1/mess"
    local_b8 = 0x73656761;          // "ages"
    if (memmem(url_ptr, url_len, &local_c0, 12) != NULL) {

        // Search body for sentinel "cch=00000"
        // Also built on stack:
        local_c0 = 0x303030303d686363;  // "cch=0000" LE
        local_b8 = 0x30;               // "0"
        offset = memmem(body_ptr, body_len, &local_c0, 9);

        if (offset != NULL) {
            // Hash the entire body
            hash_state = init(seeds...);
            hash_update(hash_state, body_ptr, body_len);
            hash_value = hash_finalize(hash_state);

            // Convert 5 nibbles to hex (SIMD)
            hex_chars = simd_nibble_to_hex(hash_value);

            // Write in-place: body[offset+4..offset+9] = hex chars
            *(uint32_t*)(body_ptr + 4 + offset) = hex_chars[0..3];
            *(char*)(body_ptr + 8 + offset) = hex_char_5th;
        }
    }
}

Key findings

1. Only the standalone binary does this — not the JS code

Confirmed experimentally:
| Runtime | Replacement active? |
|---------|-------------------|
| Official standalone binary (228MB ELF) | YES |
| bun build --compile --bytecode cli.js (homebrew standalone) | NO |
| bun cli.js (standard Bun runtime) | NO |
| npx @anthropic-ai/claude-code (npm package) | NO |

Same JavaScript code, same bytecode, same Bun standalone format — different behavior. The replacement is in Anthropic's custom Bun fork, compiled into the native Zig HTTP client.

2. Sentinel is hardcoded as stack immediates — invisible to string search

The pattern cch=00000 is built on the stack using MOV imm64 instructions:

MOV [rsp+X], 0x303030303d686363   ; "cch=0000" in little-endian
MOV byte [rsp+X+8], 0x30          ; "0" — completing "cch=00000"

This is why searching the binary for 63 63 68 3d (cch=) or 0x303030303d686363 as raw bytes in .text yields zero results — the bytes are part of instruction immediates, not contiguous string data.

3. Replacement value is a body hash, not random

The replacement value is deterministic per request body — it's a hash of the entire body with fixed seeds. But since the body changes every request (new messages, timestamps), the hash value changes too, making it appear random.

4. Only the FIRST occurrence is replaced

There is no loop — memmem finds the first match, replacement writes 5 bytes, and the function continues to build Content-Length headers. This means:

  • If messages[] (which comes BEFORE system[] in the JSON body) contains cch=00000, that gets replaced instead of the billing header in system[0]
  • The sentinel in system[0] survives — visible to the model as literal cch=00000

5. Header hashes identified

The function uses Zig's std.hash.Wyhash (seed=0, confirmed via Bun.hash()) for case-insensitive header name comparison:

| Hash | Header |
|------|--------|
| 0x58e54d60e1462681 | anthropic-version — triggers replacement |
| 0xba5173bf47c57684 | connection |
| 0x4cc2547449bddafc | upgrade |
| 0xfc8491525da4c9e3 | content-length |
| 0x308b6fc4af845531 | transfer-encoding |
| 0x2e6b2eda627d6669 | user-agent |
| 0x62b7d00b4bd17658 | accept-encoding |
| 0xfbb5acdf8484b6ef | accept |
| 0x80bd44e8947f37d2 | host |
| 0x3dfa2d55bad7bb18 | if-modified-since |

6. Sentinel history

| CC Version | cch=00000 in billing header? |
|-----------|------------------------------|
| 1.0.0 – 2.0.0 | No billing header at all |
| 2.1.0 – 2.1.34 | Billing header exists, but no cch field |
| 2.1.36 | cch=00000 introduced |
| 2.1.36 – 2.1.87 | Present in all versions |

How this causes cache invalidation

The billing header cch=00000 is placed in system[0] by the JS function DG$(). On the standalone binary, the native replacement changes 00000 to a body-hash value (e.g., a3f1b) before the request leaves the process.

In normal sessions (no sentinel in conversation content): Only system[0] is affected. Since system[0] has cache_control: null (no caching), this doesn't break the cache prefix — system[2] (main prompt with cache_control: ephemeral) and messages[] remain stable.

When conversation content contains cch=00000: This happens when:

  • CLAUDE.md discusses the billing mechanism (our research notes)
  • Read/Grep tool reads the JS bundle or binary containing the sentinel
  • User types the sentinel literally

Since messages[] comes BEFORE system[] in the JSON body, the sentinel in messages is replaced FIRST. The actual billing header in system[0] keeps 00000. But the changed value in messages breaks the cache prefix from that point onward.

Methodology

  1. MITM proxy (mitm-addon.py via mitmproxy) capturing all API request/response payloads
  2. Binary comparison: official standalone (228MB) vs homebrew bun build --compile (224MB) — same JS, different behavior
  3. Ghidra 12.0.4 reverse engineering of the standalone ELF, identifying FUN_0374d610 as the HTTP header builder with injected replacement logic
  4. radare2 disassembly for cross-referencing and function analysis
  5. Bun.hash() to verify all header name hashes (Zig Wyhash, seed=0)
  6. npm package analysis across versions 1.0.0 through 2.1.87, identifying cch=00000 introduction in v2.1.36
  7. Controlled experiments: fresh session → resume → consecutive resumes with full payload diffing
  8. 62 non-research project transcripts verified: zero accidental billing header leaks into messages

Related

This finding also explains the root cause of #34629 (cache regression on --print --resume since v2.1.69), where the deferred_tools_delta attachment introduced in v2.1.69 causes messages[0] to differ between fresh sessions and resumed sessions, independently breaking cache prefix matching.

PHPCraftdream · 3 months ago

Great work!

j0KZ · 3 months ago

i have spikes of about 200-300k tokens when this happens. this is why my cuota is being devoured

RebelSyntax · 3 months ago

I patched the cch bug, it wasnt enough. I needed to add "env": {"ENABLE_TOOL_SEARCH": "false"}. Not sure if either of those two things worked OR if i just coincidentally made these changes in the middle of anthropic doing stuff server side. The other real possibility is that even though the api reported that the cache was written in the round loop, it actually was not due to KV cache being disabled for my account? Not totally sure. but as soon as i applied both fixes, i magically stopped eating my max 20x quota, even with opus 4..6 on high... i guess YMMV.

here is the full arc. Greens are cache reads, orange are cache writes. my testing started with a fresh opus session. it started out fine, then something happened that permanently invalidated cache. you can see the reads flatlined around the system promp (11k tokens). it finally went green again after the billing header patch and the tool search setting combined. BTW: I spent 80 bucks in api credits on sonnet 4.6 medium to plow through this cause i ate up my 5hr window on max 20x. I was determined to get my usefulness back.

So hey if this helped in any way shape or form, im happy to split the cost with anthropic :P if not, well whatever i learned some things.

<img width="1768" height="568" alt="Image" src="https://github.com/user-attachments/assets/cf6de42e-8aa3-4c96-a6ec-1bf583fdab7e" />

jmarianski · 3 months ago

@RebelSyntax Check out https://github.com/anthropics/claude-code/issues/34629 as well, tools are also injected dynamically into one of the first rows of conversations, which might also lead to unknown consequences.

Your "ENABLE_TOOL_SEARCH" could have helped here to mitigate the second issue.

Gerk · 3 months ago

Also definitely affected here, have been chewing through tokens at a ridiculous rate and this explains it.

Michota · 3 months ago

now i understand why i reached Max 5x tier limits in like an hour...

Zetanova · 3 months ago

This feature req would prevent or at least rapidly show such invalid bahavior: https://github.com/anthropics/claude-code/issues/40567

cnighswonger · 3 months ago
I patched the cch bug, it wasnt enough. I needed to add "env": {"ENABLE_TOOL_SEARCH": "false"}. Not sure if either of those two things worked OR if i just coincidentally made these changes in the middle of anthropic doing stuff server side. The other real possibility is that even though the api reported that the cache was written in the round loop, it actually was not due to KV cache being disabled for my account? Not totally sure. but as soon as i applied both fixes, i magically stopped eating my max 20x quota, even with opus 4..6 on high... i guess YMMV.

Giving this a shot and here is what Claude says:

❯ And... we're back. Can you see if we picked up the new settings?

● The task tools reminder is still showing up, so the ENABLE_TOOL_SEARCH setting may not have taken effect. Let me check:

● Bash(echo $ENABLE_TOOL_SEARCH)
  ⎿  false

● The env var is set to false. The task tools reminder is a different system — that's built into Claude Code itself, not related to MCP tool search. The ENABLE_TOOL_SEARCH fix is working; the remaining noise is just the task tools nudge which is a separate feature.

I'll keep an eye on it to see if it plugs the leak here.

YigitGok · 3 months ago

I downgraded the VS Code extension to v2.1.86 and it worked for me.

SaintPepsi · 3 months ago

This typescript file runs just the second resume test:

#!/usr/bin/env bun
/**
 * test-cache.ts — Test Claude Code for resume cache regression.
 *
 * Since v2.1.69, deferred_tools_delta breaks the cache prefix on session resume.
 * Runs 3 API calls against the installed `claude` binary and compares cache metrics.
 *
 * Run: bun run scripts/test-cache.ts
 *
 * Requires: working API key (ANTHROPIC_API_KEY or ~/.claude config)
 * Cost: ~3 API calls (~$0.01-0.02 total)
 *
 * Artifacts: creates a temp dir under $TMPDIR (OS-managed cleanup) and may leave
 * small session dirs under ~/.claude/projects/-tmp-cc-cache-test-*
 */

import { $ } from "bun";
import { join } from "path";

// ── Types ──

interface ClaudeResult {
  type: string;
  result?: string;
  usage?: {
    cache_read_input_tokens?: number;
    cache_creation_input_tokens?: number;
  };
  session_id?: string;
}

interface CacheStats {
  fresh?: { cache_read: number; cache_creation: number; total: number };
  resume?: { cache_read: number; cache_creation: number; total: number };
  consecutive?: { cache_read: number; cache_creation: number; total: number };
  resume_works?: boolean;
  consecutive_grows?: boolean;
}

// ── Adapters (Bun-native, no raw Node builtin imports) ──

export async function safeJsonParse<T>(text: string): Promise<T | null> {
  return new Response(text).json().catch(() => null) as Promise<T | null>;
}

async function makeTempDir(prefix: string): Promise<string> {
  const base = Bun.env.TMPDIR ?? "/tmp";
  const template = join(base, `${prefix}XXXXXX`);
  return (await $`mktemp -d ${template}`.text()).trim();
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function buildCleanEnv(): Record<string, string> {
  const env: Record<string, string> = {};
  for (const [key, val] of Object.entries(Bun.env)) {
    if (key !== "CLAUDECODE" && val !== undefined) {
      env[key] = val;
    }
  }
  return env;
}

// ── Logging ──

function log(msg: string): void {
  console.log(`  ${msg}`);
}

function logDetail(msg: string): void {
  console.log(`    ${msg}`);
}

// ── Spinner ──

const SPINNER_FRAMES = [
  "\u280B",
  "\u2819",
  "\u2839",
  "\u2838",
  "\u283C",
  "\u2834",
  "\u2826",
  "\u2827",
  "\u2807",
  "\u280F",
];

function startSpinner(label: string): { stop: (result: string) => void } {
  let frame = 0;
  const interval = setInterval(() => {
    process.stderr.write(
      `\r  ${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${label}`,
    );
    frame++;
  }, 80);

  return {
    stop(result: string) {
      clearInterval(interval);
      process.stderr.write(`\r  ${result}\n`);
    },
  };
}

// ── Core ──

async function runClaude(
  args: string[],
  cwd: string,
  label: string,
): Promise<ClaudeResult | null> {
  const cmd = ["claude", ...args, "--output-format", "json"];
  const env = buildCleanEnv();

  log(`\u2192 ${cmd.join(" ")}`);
  const spinner = startSpinner(`${label}...`);
  const startTime = performance.now();

  const proc = Bun.spawn(cmd, {
    cwd,
    env,
    stdout: "pipe",
    stderr: "pipe",
  });

  const timeout = setTimeout(() => {
    spinner.stop("\u2717 Timed out after 120s");
    proc.kill();
  }, 120_000);

  const [stdout, stderr] = await Promise.all([
    new Response(proc.stdout).text(),
    new Response(proc.stderr).text(),
  ]);
  const exitCode = await proc.exited;
  clearTimeout(timeout);

  const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);

  if (exitCode !== 0) {
    spinner.stop(`\u2717 Failed (exit ${exitCode}, ${elapsed}s)`);
    logDetail(`stderr: ${stderr.trim().slice(0, 200)}`);
    return null;
  }

  const lines = stdout.trim().split("\n");
  for (let i = lines.length - 1; i >= 0; i--) {
    const line = lines[i].trim();
    if (!line) continue;
    const d = await safeJsonParse<ClaudeResult>(line);
    if (d?.type === "result") {
      spinner.stop(`\u2713 Done (${elapsed}s)`);
      return d;
    }
  }

  spinner.stop(`\u2717 No result in output (${elapsed}s)`);
  logDetail(
    `stdout lines: ${lines.length}, last: ${lines[lines.length - 1]?.slice(0, 80) ?? "(empty)"}`,
  );
  return null;
}

export function extractCache(result: ClaudeResult): {
  cacheRead: number;
  cacheCreation: number;
  sessionId: string | undefined;
} {
  const usage = result.usage ?? {};
  return {
    cacheRead: usage.cache_read_input_tokens ?? 0,
    cacheCreation: usage.cache_creation_input_tokens ?? 0,
    sessionId: result.session_id,
  };
}

// ── Resume Cache Test ──

async function testResumeCache(
  tmpDir: string,
): Promise<{ resumeOk: boolean; stats: CacheStats }> {
  const stats: CacheStats = {};

  log("Creating test file: readme.txt");
  await Bun.write(join(tmpDir, "readme.txt"), "ABCDEFGHIJKLMNOP\n");
  logDetail("Wrote 'ABCDEFGHIJKLMNOP' to readme.txt");

  // ── Step 1: Fresh session ──
  console.log();
  log("\u2460 Fresh session (establishes baseline cache metrics)");
  const r1 = await runClaude(
    ["-p", "Read readme.txt and tell me the first 3 characters. Be brief."],
    tmpDir,
    "Waiting for fresh session response",
  );
  if (!r1) {
    log("\u2717 Fresh session failed, cannot continue");
    return { resumeOk: false, stats };
  }

  const {
    cacheRead: cr1,
    cacheCreation: cc1,
    sessionId: sid,
  } = extractCache(r1);
  stats.fresh = { cache_read: cr1, cache_creation: cc1, total: cr1 + cc1 };
  logDetail(`cache_read:     ${cr1.toLocaleString().padStart(8)} tokens`);
  logDetail(`cache_creation: ${cc1.toLocaleString().padStart(8)} tokens`);
  logDetail(`session_id:     ${sid ?? "MISSING"}`);
  logDetail(`response:       ${(r1.result ?? "?").slice(0, 60)}`);

  if (!sid) {
    log("\u2717 No session_id returned, cannot test resume");
    return { resumeOk: false, stats };
  }

  log("Waiting 2s for cache to settle...");
  await sleep(2000);

  // ── Step 2: Resume ──
  console.log();
  log("\u2461 Resume session (should reuse cached conversation prefix)");
  logDetail(`Resuming session: ${sid}`);
  const r2 = await runClaude(
    [
      "--resume",
      sid,
      "-p",
      "What were those 3 characters? Answer from memory, no tools.",
    ],
    tmpDir,
    "Waiting for resume response",
  );
  if (!r2) {
    log("\u2717 Resume failed");
    return { resumeOk: false, stats };
  }

  const { cacheRead: cr2, cacheCreation: cc2 } = extractCache(r2);
  stats.resume = { cache_read: cr2, cache_creation: cc2, total: cr2 + cc2 };
  logDetail(`cache_read:     ${cr2.toLocaleString().padStart(8)} tokens`);
  logDetail(`cache_creation: ${cc2.toLocaleString().padStart(8)} tokens`);
  logDetail(`response:       ${(r2.result ?? "?").slice(0, 60)}`);

  if (cr2 > 0) {
    logDetail(`\u2713 Resume reused ${cr2.toLocaleString()} cached tokens`);
  } else {
    logDetail("\u26a0 Resume read 0 cached tokens (possible regression)");
  }

  log("Waiting 2s for cache to settle...");
  await sleep(2000);

  // ── Step 3: Consecutive resume ──
  log("\u2462 Consecutive resume (cache should grow with conversation)");
  logDetail(`Resuming session again: ${sid}`);
  const r3 = await runClaude(
    ["--resume", sid, "-p", "How many turns have we had? Just the number."],
    tmpDir,
    "Waiting for consecutive resume response",
  );
  if (!r3) {
    log("\u2717 Consecutive resume failed");
    return { resumeOk: false, stats };
  }

  const { cacheRead: cr3, cacheCreation: cc3 } = extractCache(r3);
  stats.consecutive = {
    cache_read: cr3,
    cache_creation: cc3,
    total: cr3 + cc3,
  };
  logDetail(`cache_read:     ${cr3.toLocaleString().padStart(8)} tokens`);
  logDetail(`cache_creation: ${cc3.toLocaleString().padStart(8)} tokens`);
  logDetail(`response:       ${(r3.result ?? "?").slice(0, 60)}`);

  // ── Analysis ──
  log("\u2500 Analysis \u2500");
  const consecutiveGrows = cr3 > cr2;
  const consecutiveCreationDrops = cc3 < cc2 * 0.8;

  logDetail(
    `cache_read grew:        ${consecutiveGrows ? "\u2713 yes" : "\u2717 no"} (${cr2.toLocaleString()} \u2192 ${cr3.toLocaleString()})`,
  );
  logDetail(
    `cache_creation dropped: ${consecutiveCreationDrops ? "\u2713 yes" : "\u2717 no"} (${cc2.toLocaleString()} \u2192 ${cc3.toLocaleString()}, threshold: <${(cc2 * 0.8).toFixed(0)})`,
  );

  const resumeOk = consecutiveGrows && consecutiveCreationDrops;
  stats.resume_works = resumeOk;
  stats.consecutive_grows = consecutiveGrows;

  if (resumeOk) {
    logDetail("\u2713 Cache is growing between resumes (healthy)");
  } else {
    logDetail("\u2717 Cache is NOT growing (regression detected)");
  }

  return { resumeOk, stats };
}

// ── Main ──

async function main(): Promise<number> {
  console.log("\u2550".repeat(60));
  console.log("  Claude Code Resume Cache Test");
  console.log("\u2550".repeat(60));
  console.log();

  log("Setting up...");
  const tmpDir = await makeTempDir("cc-cache-test-");
  logDetail(`Temp dir: ${tmpDir}`);
  logDetail("CLAUDECODE env stripped: yes");
  console.log();

  // ── Resume cache test ──
  console.log("\u2500".repeat(60));
  console.log("  Resume cache regression test");
  console.log("\u2500".repeat(60));
  const { resumeOk, stats } = await testResumeCache(tmpDir);
  console.log();

  // ── Verdict ──
  console.log("\u2550".repeat(60));
  console.log("  RESULTS");
  console.log("\u2550".repeat(60));

  if (resumeOk) {
    console.log("  \u2713 RESUME CACHE: WORKING");
    const cr = stats.resume?.cache_read ?? 0;
    logDetail(
      `Resume cache_read=${cr.toLocaleString()} (includes conversation history)`,
    );
  } else {
    console.log("  \u2717 RESUME CACHE: BROKEN");
    const cr = stats.resume?.cache_read ?? 0;
    const cc = stats.resume?.cache_creation ?? 0;
    logDetail(`Resume cache_read=${cr.toLocaleString()} (system prompt only)`);
    logDetail(
      `cache_creation=${cc.toLocaleString()} (full conversation rebuilt)`,
    );
    logDetail(`Each resume pays ~$${((cc * 0.3) / 1e6).toFixed(4)} extra`);
    logDetail("Issue: https://github.com/anthropics/claude-code/issues/34629");
  }
  console.log();

  if (stats.consecutive_grows) {
    console.log(
      "  \u2713 CONSECUTIVE RESUME: WORKING (cache grows between resumes)",
    );
  } else if (stats.consecutive) {
    console.log("  \u26a0 CONSECUTIVE RESUME: NOT GROWING");
  }
  console.log();

  console.log("\u2500".repeat(60));
  if (resumeOk) {
    console.log("  \u2605 No known cache bugs detected");
  } else {
    console.log("  \u26a0 Bug detected \u2014 see details above");
  }
  console.log("\u2500".repeat(60));

  console.log(`\n  Temp dir: ${tmpDir} (OS-managed cleanup)`);

  return resumeOk ? 0 : 1;
}

process.exit(await main());

Result for me (v2.1.87):

════════════════════════════════════════════════════════════
  Claude Code Resume Cache Test
════════════════════════════════════════════════════════════

  Setting up...
    Temp dir: /var/folders/50/d1gfnml10y151m4c5lsnl4sw0000gp/T/cc-cache-test-HotUhB
    CLAUDECODE env stripped: yes

────────────────────────────────────────────────────────────
  Resume cache regression test
────────────────────────────────────────────────────────────
  Creating test file: readme.txt
    Wrote 'ABCDEFGHIJKLMNOP' to readme.txt

  ① Fresh session (establishes baseline cache metrics)
  → claude -p Read readme.txt and tell me the first 3 characters. Be brief. --output-format json
  ✓ Done (18.1s)fresh session response...
    cache_read:       48,667 tokens
    cache_creation:   48,850 tokens
    session_id:     66a7a9fd-1f30-4965-89fb-7708b709a6a9
    response:       The first 3 characters are **ABC**.
  Waiting 2s for cache to settle...

  ② Resume session (should reuse cached conversation prefix)
    Resuming session: 66a7a9fd-1f30-4965-89fb-7708b709a6a9
  → claude --resume 66a7a9fd-1f30-4965-89fb-7708b709a6a9 -p What were those 3 characters? Answer from memory, no tools. --output-format json
  ✓ Done (10.4s)resume response...
    cache_read:       12,228 tokens
    cache_creation:   36,671 tokens
    response:  (...truncated)
    ✓ Resume reused 12,228 cached tokens
  Waiting 2s for cache to settle...
  ③ Consecutive resume (cache should grow with conversation)
    Resuming session again: 66a7a9fd-1f30-4965-89fb-7708b709a6a9
  → claude --resume 66a7a9fd-1f30-4965-89fb-7708b709a6a9 -p How many turns have we had? Just the number. --output-format json
  ✓ Done (9.4s) consecutive resume response...
    cache_read:       12,228 tokens
    cache_creation:    9,046 tokens
    response:  (...truncated)

3 turns (in
  ─ Analysis ─
    cache_read grew:        ✗ no (12,228 → 12,228)
    cache_creation dropped: ✓ yes (36,671 → 9,046, threshold: <29337)
    ✗ Cache is NOT growing (regression detected)

════════════════════════════════════════════════════════════
  RESULTS
════════════════════════════════════════════════════════════
  ✗ RESUME CACHE: BROKEN
    Resume cache_read=12,228 (system prompt only)
    cache_creation=36,671 (full conversation rebuilt)
    Each resume pays ~$0.0110 extra
    Issue: https://github.com/anthropics/claude-code/issues/34629

  ⚠ CONSECUTIVE RESUME: NOT GROWING

────────────────────────────────────────────────────────────
  ⚠ Bug detected — see details above
────────────────────────────────────────────────────────────

https://github.com/anthropics/claude-code/issues/34629

coygeek · 3 months ago
RebelSyntax · 3 months ago
Update from Anthropic employee: https://x.com/trq212/status/2038728677270393080

of course its not the root cause, its the stop gap that is preventing massive compute loss. root cause is gonna take the engineering team that has the context of at least 20 possibly more release cycles to pinpoint. but hey, we know now that there is a cause, so the effect is real and its nothing to do with quotas, or am i a hallucinating human>

cnighswonger · 3 months ago
This typescript file runs just the second resume test:

FWIW, confirmed:

$ claude --version
2.1.87 (Claude Code)
● Test confirms the bug. Resume cache is broken:

  - Fresh session: 20,974 cache_read, 4,163 cache_creation
  - Resume: 20,974 cache_read (same — only system prompt cached, conversation not reused)
  - Consecutive: 20,974 cache_read (no growth — conversation history being rebuilt every time)

  Cache reads stay flat at ~21K tokens (system prompt only) while cache creation stays at ~4K per resume (rebuilding the conversation prefix each time). The bug from issue #34629 is confirmed on this install.
cnighswonger · 3 months ago

Gotta love @claude ...

❯ Thanks. That's a nasty bug. It is causing massive token bleeds. People are not happy at all. Paying customers are bleeding $$$$.

● Yeah, it's bad. Every --resume rebuilds the entire conversation prefix instead of reading it from cache — and that's billed at the full
  cache_creation rate (3.75x the normal input rate). For a long session like ours, that's significant money on every resume.

  The ENABLE_TOOL_SEARCH=false setting we added helps with the deferred tools noise, but this cache regression is a separate issue at the API layer. Nothing we can do from our side except avoid unnecessary resumes and keep the issue visible.
cnighswonger · 3 months ago
> This typescript file runs just the second resume test: FWIW, confirmed: `` $ claude --version 2.1.87 (Claude Code) ` ` ● Test confirms the bug. Resume cache is broken: - Fresh session: 20,974 cache_read, 4,163 cache_creation - Resume: 20,974 cache_read (same — only system prompt cached, conversation not reused) - Consecutive: 20,974 cache_read (no growth — conversation history being rebuilt every time) Cache reads stay flat at ~21K tokens (system prompt only) while cache creation stays at ~4K per resume (rebuilding the conversation prefix each time). The bug from issue #34629 is confirmed on this install. ``

Same results with 2.1.88 (Claude Code).

RebelSyntax · 3 months ago

ya .88 broke my patch, had to reapply. can see here the moment .88 auto updated. now im starting to wonder, if that is intentional or what? are we supposed to NOT be using the input cache? im confused...

i had 250k tokens caches in context (the big green hump). probably 25-30 tool calls fired off on the next turn after the .88 auto update. brand new quota shot my usage up to 70% with 4h 30m remain. ouch!

<img width="1913" height="829" alt="Image" src="https://github.com/user-attachments/assets/2620f05d-4b2d-4005-b133-67acad21efab" />

yurukusa · 3 months ago

This cache invalidation issue is frustrating — you're essentially paying double for tokens when the cache drops.
What's likely happening:
Claude Code uses prompt caching to avoid re-sending the full conversation on each turn. But certain events invalidate the cache:

  • Tool results being modified after the fact (e.g., billing hash injection — see #40652)
  • Context compression (/compact or auto-compact)
  • System prompt changes (e.g., new CLAUDE.md content loaded mid-session)
  • Session recovery after errors

When the cache invalidates, the entire conversation history gets re-sent as fresh input tokens, which can cause a massive spike in token usage.
Workarounds to minimize cache invalidation:

  1. Keep conversations shorter: Start new sessions for new tasks rather than continuing a single long session. Shorter history = less to re-cache when invalidation happens.
  2. Compact proactively: Use /compact at natural breakpoints (after completing a task) rather than letting auto-compact trigger at 100% context.
  3. Avoid editing settings mid-session: Changes to .claude/settings.json or CLAUDE.md during a session can trigger system prompt reloads.
  4. Monitor with a hook: You can track cache behavior with a PostToolUse hook:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hook": "bash -c 'echo \"[$(date +%H:%M:%S)] tool completed\" >> /tmp/claude-cache-log.txt'"
      }
    ]
  }
}

This won't prevent invalidation, but helps correlate when it happens with what Claude was doing.

  1. Check for the billing hash issue: If you see cch= appearing in your tool results, that's the mutation described in #40652 and may be contributing to cache invalidation.

What would help from Anthropic's side: Visibility into why the cache was invalidated (a /stats enhancement or debug flag) would help users identify and avoid the triggers.

jeannen · 3 months ago

I really hope they will reset the weekly limits of everyone impacted, or at minimum give what should have been

motyar · 3 months ago

Ignore the bugs that helps more $$$$ come in.

SaintPepsi · 3 months ago
ya .88 broke my patch, had to reapply. can see here the moment .88 auto updated. now im starting to wonder, if that is intentional or what? are we supposed to NOT be using the input cache? im confused... i had 250k tokens caches in context (the big green hump). probably 25-30 tool calls fired off on the next turn after the .88 auto update. brand new quota shot my usage up to 70% with 4h 30m remain. ouch! <img width="1913" height="829" alt="Image" src="https://github.com/user-attachments/assets/2620f05d-4b2d-4005-b133-67acad21efab" />

What tool is this? Would like to see what mine looks like

al3rez · 3 months ago

just hit my rate limit in less than 30 min lol

UpayanGhosh · 3 months ago

What is the current status of this bug? And for normal use what is the current temporary fix suggested?

bawse · 3 months ago

Experiencing the same issue - this is critical for organizations and individuals alike. Anthropic should definitely reimburse all credits since the issue was introduced.

UpayanGhosh · 3 months ago
Experiencing the same issue - this is critical for organizations and individuals alike. Anthropic should definitely reimburse all credits since the issue was introduced.

Agree. For now what the community is suggesting? Like what is the temporary work around? Does npx @anthropic-ai/claude-code solves the problem for now?

bawse · 3 months ago
> Experiencing the same issue - this is critical for organizations and individuals alike. Anthropic should definitely reimburse all credits since the issue was introduced. Agree. For now what the community is suggesting? Like what is the temporary work around? Does npx @anthropic-ai/claude-code solves the problem for now?

I read earlier in the thread that someone had to downgrade their version of VS Code (perhaps this is because it points to an older version of CC which doesn't have this bug). However, there's so many different mediums through which people use Claude that there's no real workaround until this is patched by the Anthropic team.

achton · 3 months ago

The only solution I know of right now is to downgrade to 2.1.68 (npm install -g @anthropic-ai/claude-code@2.1.68) and run CC via npx.

UpayanGhosh · 3 months ago
The only solution I know of right now is to downgrade to 2.1.68 (npm install -g @anthropic-ai/claude-code@2.1.68) and run CC via npx.

Ok so we have to roll back to a much older version. Does that mean this was a hidden bug that was introduced back then and is getting our attention now?

jippi · 3 months ago
- Fixed prompt cache misses in long sessions caused by tool schema bytes changing mid-session changelog 2.1.88
UpayanGhosh · 3 months ago
> * Fixed prompt cache misses in long sessions caused by tool schema bytes changing mid-session > changelog 2.1.88

Ok so the bug is now closed?

dkodr · 3 months ago

Could CLAUDE_CODE_ATTRIBUTION_HEADER also have something to do with this?
https://www.reddit.com/r/ClaudeAI/comments/1s8j09e/psa_your_claude_code_is_wasting_tokens_on_every/
I can't find this env variable documented anywhere.

TomasHubelbauer · 3 months ago

Thanks to the today's Claude Code leak, you don't need to check the documentation, you can just check the source ;-)

https://github.com/instructkr/claude-code/blob/4b9d30f7953273e567a18eb819f4eddd45fcc877/src/constants/system.ts#L53

whiletrue0x · 3 months ago

I did some source analysis on this and found one of the contributing factors.

Since v2.1.69, cli.js injects an x-anthropic-billing-header string as the first block of the system prompt:

x-anthropic-billing-header: cc_version=2.1.88.a3f; cc_entrypoint=cli; cch=00000;

The .a3f is a 3-char SHA-256 hash derived from the first user message:

// Deobfuscated
const chars = [4, 7, 20].map(i => firstUserMessage[i] || "0").join("");
return sha256("59cf53e54c78" + chars + version).slice(0, 3);

This hash changes per conversation and per subagent (each Agent tool call has its own message context). Since prompt cache requires 100% identical prefixes, the system prompt block (~12K tokens) gets cache_creation instead of cache_read on every new conversation and every subagent call.

A/B test results (4 sessions each, --print mode):

| | cache_read | cache_creation | hit ratio |
|--|-----------|----------------|-----------|
| Header ON (default) | ~11,272 | ~12,200 | 48% |
| Header OFF | ~23,475 | 0 | 99.98% |

The ~11K that caches in both cases is the tools block. The ~12K system prompt only caches when the header is removed.

Workaround:

export CLAUDE_CODE_ATTRIBUTION_HEADER=false

This env var is a proper feature toggle in the source (not a hack). Multiple third-party projects already use it.

Full analysis, test script, and raw data: https://github.com/motiful/cc-cache-audit

jmarianski · 3 months ago

@whiletrue0x From what I recall this relates to the other issue I mention in the comments (https://github.com/anthropics/claude-code/issues/34629) not directly to this one. It invalidates ONE message in the chain after resume, subsequent ones are correctly cached because first user message doesn't change.

cnighswonger · 3 months ago
Workaround: export CLAUDE_CODE_ATTRIBUTION_HEADER=false This env var is a proper feature toggle in the source (not a hack). Multiple third-party projects already use it. Full analysis, test script, and raw data: https://github.com/motiful/cc-cache-audit

FWIW on my system:

● That's excellent analysis. The billing header includes a per-conversation hash that changes the system prompt prefix, breaking cache on every new session and subagent call. The fix is a single env var:

export CLAUDE_CODE_ATTRIBUTION_HEADER=false

This is safe — it's a built-in feature toggle, not a hack. The 48% → 99.98% cache hit ratio improvement is dramatic.

Add it to your ~/.claude/settings.json:

{
"env": {
"ENABLE_TOOL_SEARCH": "false",
"CLAUDE_CODE_ATTRIBUTION_HEADER": "false"
}
}

This won't fix the mid-session cache collapse (that's the other part of the bug), but it should eliminate the cache miss on every resume and subagent spawn. Worth trying without exiting this session — the env var would take effect on the next resume.

❯ Can you add that for us?

Read 1 file (ctrl+o to expand)

● Update(~/.claude/settings.json)
⎿  Added 2 lines, removed 1 line
5 ],
6 "effortLevel": "high",
7 "env": {
8 - "ENABLE_TOOL_SEARCH": "false"
8 + "ENABLE_TOOL_SEARCH": "false",
9 + "CLAUDE_CODE_ATTRIBUTION_HEADER": "false"
10 }
11 }

● Done. Takes effect on next resume. Should cut the cache_creation on resume significantly — the ~12K system prompt tokens will cache_read instead of cache_create each time.

✻ Churned for 30s

❯ Here goes nothing. BRB.

● Good luck. Watching the usage delta.

❯ /exit
⎿ Catch you later!

❯ Back.

● How's the usage looking?

❯ We were at 42% before restart. Waiting for the UI to update.

● Standing by.

❯ 59%... Ouch. That's absolutely crazy. No wonder the world is on fire.

● 17% of your daily allocation burned on a single resume. That's unsustainable.

Until Anthropic fixes this, we should minimize resumes — work straight through, avoid exits unless absolutely necessary. The bug is assigned and has massive community pressure. Not much else we can do from our side.

agolwa · 3 months ago

Is the bug closed and are we back to normal ? Its still showing me 2.1.87 is the latest version

cnighswonger · 3 months ago

@qing-ant How about a status update?

Zetanova · 3 months ago

Does anthropic reimburse us again?

The optimal moment would be this Fr. 11:00 GMT

arambert · 3 months ago

This is becoming a very serious issue. Even if Anthropic fixes this in the next few hours, a lot of paying users have already burned their weekly allowance. A lot of companies trusted Anthropic with their development workflow and lost entire days of engineering work, I'll let you compute how much money that means.
It would be smart for Anthropic to build metrics like avg_credit(or token of each class like cache write/read etc.)_consumption_per_hour/day/week for different pools of users and check for abnormal variations.

golgocat · 3 months ago

I'm also affected by this issue. My account (API billing) shows a clear discrepancy between API key-level usage and actual invoices:

  • Total API key usage across all keys: ~$291
  • Invoices for March 30–31 alone: $424.82 (three separate invoices on March 31: $103.72, $108.01, $112.79, plus $100.30 on March 30)

Multiple invoices per day is abnormal, and the total invoiced amount far exceeds what the API key usage dashboard reports. I use Claude Code daily, so the cache invalidation bug is likely inflating my costs significantly.

I've also filed a support ticket with Anthropic billing (support@anthropic.com) referencing this issue.

Please investigate and consider reimbursement for affected users. This is a platform-side bug, not user error.

FlorianBruniaux · 3 months ago

Based on @jmarianski's RE analysis and @whiletrue0x's attribution header findings, I put together a slash command that audits for all three issues in one pass.

What it checks (~20s):

  • Bug 1: cch=00000 sentinel in static config files (CLAUDE.md, skills, hooks, commands)
  • Bug 2: automated or manual --resume/--continue usage (hooks, settings, shell profiles, scripts)
  • Bug 3: whether CLAUDE_CODE_ATTRIBUTION_HEADER is already disabled

Install:

curl -o ~/.claude/commands/check-cache-bugs.md \
  https://raw.githubusercontent.com/FlorianBruniaux/claude-code-ultimate-guide/main/examples/commands/check-cache-bugs.md

Then run /check-cache-bugs in any Claude Code session.

A few things I found while testing on my own setup:

  • CLAUDE_CODE_ATTRIBUTION_HEADER accepts "0", "false", "no", or "off" — confirmed in the source (j0() in cli.js). All equivalent.
  • The command skips .jsonl files intentionally — they always contain the sentinel after discussing this bug, so they're noise, not signal.
  • Stale Homebrew npm installs (/opt/homebrew/bin/claude) show up as a second binary and can be misleading. The command flags them separately.

No claim this covers everything. Sharing in case it saves someone time while the fix ships. If you spot gaps or wrong assumptions, happy to adjust.

golgocat · 3 months ago

Ran @FlorianBruniaux's /check-cache-bugs audit — confirmed Bug 3 ACTIVE on v2.1.87 (standalone, macOS arm64). Bugs 1 and 2 clean.

Applied the workaround:

"env": { "CLAUDE_CODE_ATTRIBUTION_HEADER": "false" }

For reference, my billing discrepancy was ~$291 API key usage vs $424.82 invoiced over 2 days (March 30–31). Thanks to @whiletrue0x for the root cause analysis and @FlorianBruniaux for the diagnostic command.

genesiscz · 3 months ago
Based on @jmarianski's RE analysis and @whiletrue0x's attribution header findings, I put together a slash command that audits for all three issues in one pass. What it checks (~20s): Bug 1: cch=00000 sentinel in static config files (CLAUDE.md, skills, hooks, commands) Bug 2: automated or manual --resume/--continue usage (hooks, settings, shell profiles, scripts) Bug 3: whether CLAUDE_CODE_ATTRIBUTION_HEADER is already disabled Install: curl -o ~/.claude/commands/check-cache-bugs.md \ https://raw.githubusercontent.com/FlorianBruniaux/claude-code-ultimate-guide/main/examples/commands/check-cache-bugs.md Then run /check-cache-bugs in any Claude Code session. A few things I found while testing on my own setup: CLAUDE_CODE_ATTRIBUTION_HEADER accepts "0", "false", "no", or "off" — confirmed in the source (j0() in cli.js). All equivalent. The command skips .jsonl files intentionally — they always contain the sentinel after discussing this bug, so they're noise, not signal. Stale Homebrew npm installs (/opt/homebrew/bin/claude) show up as a second binary and can be misleading. The command flags them separately. No claim this covers everything. Sharing in case it saves someone time while the fix ships. If you spot gaps or wrong assumptions, happy to adjust.

You are missing

 ---
  name: check-cache-bugs                                                  
   description: Audit Claude Code setup for cache bugs                      
 ---

so it doesn't get recognized..❯ Unknown skill: check-cache-bug :)

3rd · 3 months ago
This feature req would prevent or at least rapidly show such invalid bahavior: #40567

A basic software testing practice would better prevent these bugs.

FlorianBruniaux · 3 months ago

@genesiscz thanks ! fixed https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/examples/commands/check-cache-bugs.md

I'll add it to all the examples https://cc.bruniaux.com/examples/ and will try to do an article on this on my blog

UpayanGhosh · 3 months ago

What is the status of this BUG? Are we still having the same issue of more token consumption!!

jmarianski · 3 months ago

Reminder: this bug has marginal impact on your session tokens. It has marginal effect on claude code except when you deliberately poison it by forcing it to deminify code.

There are other bugs that are more relevant to token consumption, like resume bug discovered between 2.1.68 and 2.1.69. Also everything points to the fact that it is a combination of effects like "extract memories" behind a feature flag "tengu_passport_quail", huge server loads, which could invalidate both caches when extract memories request gets triggered.

Currently I'm having no issues with cache on 2.1.68 through npm, however I'm also not targetted by tengu_passport_quail. But I would assume that issues could arise from people having cache misses when sending 2 or more requests in parallel. Example from my workload: my brilliant agent decided to spawn 7 subagents to browse through the leaked repo and I found out that when running parallel they might crash each other out:

21:11:17 0 57122 3 109 haiku-4-5-20251001 tool_use <-- spawn
21:11:17 0 57092 3 93 haiku-4-5-20251001 tool_use <-- spawn
21:11:18 0 57143 3 101 haiku-4-5-20251001 tool_use <-- spawn
21:11:18 0 57107 3 103 haiku-4-5-20251001 tool_use <-- spawn
21:11:18 0 57061 3 237 haiku-4-5-20251001 tool_use <-- spawn
21:11:19 0 57100 3 98 haiku-4-5-20251001 tool_use <-- spawn
21:11:19 57122 564 5 102 haiku-4-5-20251001 tool_use <-- second turn
21:11:19 57092 616 5 94 haiku-4-5-20251001 tool_use <-- second turn
21:11:19 8995 48767 5 115 haiku-4-5-20251001 tool_use <-- second turn (crash)
21:11:19 8995 48738 5 97 haiku-4-5-20251001 tool_use <-- second turn (crash)
21:11:20 57061 438 7 204 haiku-4-5-20251001 tool_use <-- second turn
21:11:20 57686 941 4 83 haiku-4-5-20251001 tool_use <-- second/third turn
21:11:20 8995 48726 5 132 haiku-4-5-20251001 tool_use <-- second turn (crash)
21:11:20 57708 164 4 80 haiku-4-5-20251001 tool_use <-- third turn?

So yeah, there are reasons why Anthropic wants you to get off their servers. They're gasping for air. And recent updates are not making it easy on them.

fivedollarfridays · 3 months ago

Measured impact with heavy skills/hooks usage

Adding concrete session data from a setup with 14 custom skills (deferred tools) and 4 active hooks.

Environment

  • Claude Code 2.1.88 (also observed on earlier versions)
  • macOS 26.3, Apple Silicon
  • 14 project-level skills in .claude/skills/ (each loaded as deferred tools)
  • 4 hooks: PreToolUse (2), PostToolUse (1), UserPromptSubmit (1), PreCompact (1)
  • Long sessions (184+ cache metric entries per session)

Session JSONL cache analysis

Pulled directly from ~/.claude/projects/ session files. Every resume event shows cache_read dropping to 0 and the full context rebuilding as cache_creation:

| Entry | cache_read | cache_creation | Ratio | Event |
|-------|-----------|---------------|-------|-------|
| 102 | 84,164 | 174 | 100% | Normal |
| 103 | 0 | 87,176 | 0% | Resume — full rebuild |
| 105 | 87,176 | 561 | 99% | Recovered |
| 151 | 106,571 | 72 | 100% | Normal |
| 152 | 0 | 107,070 | 0% | Resume — full rebuild |
| 153 | 107,070 | 598 | 99% | Recovered |
| 166 | 115,989 | 221 | 100% | Normal |
| 167 | 0 | 118,523 | 0% | Resume — full rebuild |

Each bold row is ~100-118K tokens of unnecessary cache_creation that should have been cache_read. In a single session with 3-4 resumes, that is 300-400K tokens of avoidable cost.

Root cause confirmation

The community analysis of db8 stripping deferred_tools_delta attachment records from session JSONL aligns with what we observe. Our setup is worst-case because:

  • 14 skills = 14 deferred tool announcements stripped on write
  • On resume, all 14 are re-announced, shifting every message position
  • Cache prefix breaks because the message array structure changes
  • This compounds: the longer the session, the more expensive each rebuild

Scaling factor

More skills/deferred tools = worse impact. Users with 1-2 skills may barely notice. Users with 10+ skills (common in framework setups like ours) see the full 0% cache ratio on every resume.

The fix

The two-line addition to the attachment filter to preserve deferred_tools_delta and mcp_instructions_delta records is logically correct. This keeps the tool announcement history in the session file so resume can compute the delta correctly instead of re-announcing everything.

Related: #34629, and we also filed #41530 for a separate skills regression in 2.1.88.

FlorianBruniaux · 3 months ago

Bug 2 root cause correction + audit command

Following up on the analysis in this thread with two additions.

Bug 2 — root cause correction

The --resume / --continue cache regression is not a position mismatch when restoring the conversation. The actual cause is earlier: the session JSONL writer strips deferred_tools_delta attachment records before writing to disk. On resume, those records are absent; the deferred tools layer has no prior announcement history and re-announces all tools from scratch. This shifts every message position in the restored conversation, breaking the messages-level cache prefix entirely.

The fix should target the writer path (preserve DTD records on write), not the restore path. The compaction path already handles this correctly ; it re-announces with an empty set to force a clean diff. The session writer just needs the same treatment.

This is consistent with @fivedollarfridays' data: every resume event shows cache_read: 0 followed by a full cache_creation rebuild (87–118K tokens), regardless of session content. With 10+ skills loaded, 3–4 resumes per session = 300–400K tokens of avoidable cost.

Audit command

For anyone wanting to check their exposure: I've updated /check-cache-bugs audit command to the Claude Code Ultimate Guide. It covers all three bugs (version check, sentinel scan, resume usage detection, attribution header status) and outputs a structured report with per-bug status and fixes.

Command: https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/examples/commands/check-cache-bugs.md

Known Issues doc: https://cc.bruniaux.com/guide/known-issues/

Credit to @whiletrue0x and @jmarianski for the original RE work that made this analysis possible.

jmarianski · 3 months ago

Just curious, @FlorianBruniaux - wouldn't executing this skill make your session cache-invalidated if cch=00000 succesfully appeared in your context? Have you considered a utility that should check it against "non-current" version or in the background in a way that wouldn't infect the context?

Also, a sidenote, I was the author of those reddit threads* that started our wild goose chase (u/skibidi-toaleta-2137), you can correct it (remove?) in the skill description.

*- besides Claude, duh

FlorianBruniaux · 3 months ago

@jmarianski, both good catches.

Cache contamination: yes, loading the skill inline injects cch= strings into messages[], which is the Bug 1 trigger on standalone binaries. Updated with two options:

Fresh session start: run the audit as the very first action before any other context loads. The skill content is the first thing in messages[], so no prior sentinel to hit.

Print mode (claude -p): one-shot invocation outside any persistent session: bash claude -p "$(cat ~/.claude/commands/check-cache-bugs.md)" Runs isolated with no messages[] carryover, which eliminates contamination risk. Simpler than a background agent and covers the same isolation.

Reddit credit: fixed. You were listed as both u/skibidi-toaleta-2137 and @jmarianski, which made it look like two separate contributors. Consolidated to @jmarianski (GitHub / u/skibidi-toaleta-2137, r/ClaudeAI). The Reddit threads kicked off the whole community RE effort so keeping the handle seems right, but happy to drop it to just @jmarianski if you prefer.

patrikmanii · 3 months ago

Will Anthropic finally release a patch, it's been like 3 days... has someone seen any updates from there side?

arizonawayfarer · 3 months ago

Setting CLAUDE_CODE_ATTRIBUTION_HEADER to false helps significantly - I wouldn't call it a perfect fix.. but yeah.. it's a significant difference

Zetanova · 3 months ago

@FlorianBruniaux fix for the slush command
All three known bugs mitigated and safe, but still there is something off

But I think there is one more bug out there at least about the cache misses.

ForAllKid · 3 months ago

<img width="2096" height="480" alt="Image" src="https://github.com/user-attachments/assets/2c0cd77d-6624-44c6-8541-3f601bd019be" />

845 input tokens. 90M cache read tokens. $54.59 in one session.
@anthropic — when is this getting fixed? Give us a date.

CaptainCrouton89 · 3 months ago

Jesus christ don't make me switch to codex. Normal programming on max plan and cooked

martinambrus · 3 months ago

Starting to think the community should fork and start maintaining their own Claude Code project. Anthropic is really doing whatever the hell they want with their code and infrastructure lately and ignoring their paying users a lot. @claude maybe triage this very important issue better?

patrikmanii · 3 months ago
Jesus christ don't make me switch to codex. Normal programming on max plan and cooked

That's what I did, at least for now. It's funny how they promote themselves, but are not able to fix key issues in there code in 96+h, they are just neglecting the fact that there is even something wrong, no statement/blog post confirming "sorry there is a issue".

@anthropics

agolwa · 3 months ago

@jmarianski @martinambrus with the claude code map leak and the new qwen model...an opensource local setup can nicely support expensive plans (which seems to be the future...given anthrpoic clearly is not solving this)

joseph-holland · 3 months ago
Starting to think the community should fork and start maintaining their own Claude Code project. Anthropic is really doing whatever the hell they want with their code and infrastructure lately and ignoring their paying users a lot.

Well OpenCode other whatever other open-source AI coding tools are 100% going to benefit from the leak and compete directly with CC. I've been saying the harness around the models is the moat for a while, I mean we have Opus and Sonnet in GitHub Copilot but many people choose to pay multiple times more for Claude subscriptions.

I've been recommending Claude for a while now and it's my daily driver, but with this I'm seriously considering moving off it. Between this issue, the resume cache problem and the limit changes it's a disaster right now. I'm hitting limits mulitple times per day, hit them five time a couple of days ago and weekly quota is cooked now.

Anthropic, the cat is out of the bag. Just open source the CC codebase and let others contribute too; stuff like this would get fixed faster.

joseph-holland · 3 months ago
> Jesus christ don't make me switch to codex. Normal programming on max plan and cooked That's what I did, at least for now. It's funny how they promote themselves, but are not able to fix key issues in there code in 96+h, they are just neglecting the fact that there is even something wrong, no statement/blog post confirming "sorry there is a issue". @anthropics

Why fix this if many people don't know about it and I'm sure it's driving upgrades to higher tier subs and more API spend.

loki-sinclair-hdruk · 3 months ago

32% consumed of my weekly quota (Pro - which reset yesterday), have maxed out session limits twice today alone, and all I've done is ask it to write some Cypress tests for a web app. I could run multi-agent operations via CLI, writing multiple projects and never touch the end of my session limit within the 5hrs, until recently.

foucal59 · 3 months ago
never touch the end of my session limit within the 5hrs, until recently.

Same here… I use https://github.com/IgniteStudiosLtd/claude-usage-tool (very handy tool) to track my usage from week to week, and I've noticed that my usage has increased fivefold.

HatmanStack · 3 months ago
> Jesus christ don't make me switch to codex. Normal programming on max plan and cooked That's what I did, at least for now. It's funny how they promote themselves, but are not able to fix key issues in there code in 96+h, they are just neglecting the fact that there is even something wrong, no statement/blog post confirming "sorry there is a issue".

Don't you think this has to be something deeper related to the IPO and restricting token usage for the subscription model. It seems too prevalent and persistent to be a coding bug.

waelmas · 3 months ago

Would have been nice if at least Anthropic increased limits for Max users until a fix is officially released. Been burning through the limits at extreme speed (probably 7-10x compared to last few weeks).

adamlucia · 3 months ago

1) When I signed up, we agreed to 2x tokens. Is that no longer the case? Because it feels like it.

2) I'm getting to the point where I'm hitting my weekly limit (something that I've never done). Using some of the tips I've picked up about how to conserve tokens has worked to a point, but I blew through my session limits left and right at the beginning of the week, interrupting any inertia I had in the process. How is there no response from Anthropic or compensation for this mistake?

leiyangyou · 3 months ago

Is this to do with resume in particular?

On Thu, 2 Apr 2026 at 23:42, Adam Lucia @.***> wrote:

adamlucia left a comment (anthropics/claude-code#40524) <https://github.com/anthropics/claude-code/issues/40524#issuecomment-4178768912> 1. When I signed up, we agreed to 2x tokens. Is that no longer the case? Because it feels like it. 2. I'm getting to the point where I'm hitting my weekly limit (something that I've never done). Using some of the tips I've picked up about how to conserve tokens has worked to a point, but I blew through my session limits left and right at the beginning of the week, interrupting any inertia I had in the process. How is there no compensation for this mistake? — Reply to this email directly, view it on GitHub <https://github.com/anthropics/claude-code/issues/40524?email_source=notifications&email_token=AAAZPTI4NUONUMABPGZJFBD4T2C53A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMJXHA3TMOBZGEZKM4TFMFZW63VGNVQW45LBNSSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-4178768912>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAAZPTIOUXE64NUXAEGJ3E34T2C53AVCNFSM6AAAAACXEVZMDKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DCNZYG43DQOJRGI> . You are receiving this because you are subscribed to this thread.Message ID: @.***>
dagelf · 3 months ago

--resume also an issue but this is a bug in the binary, run Claude Code via npx @anthropic-ai/claude-code instead.

kyzzen · 3 months ago

Built two hooks to deal with this on the native binary. Sharing in case useful.

The content contamination angle — @jmarianski's RE covers the billing header rewrite, but the sentinel scan hits the full serialized request body including messages[]. So if a tool result pulls in a file containing the sentinel (the binary itself, config files, even this issue page), it enters the conversation and gets matched on every subsequent request. That's what makes sessions permanently cold — it's not just the header, it's the conversation content mutating per-request.

What it costs — I've been logging per-turn cache tokens via a statusline hook. On affected sessions: cache hit rate dropped from 91% to 75%, cost per API call doubled ($0.37 → $0.91). One session ran up $30 with 12.5M wasted cache_create tokens before I caught it.

PreToolUse hook — sentinel guard. Intercepts Read and Grep calls, scans the target file for the sentinel before the tool executes. If found, blocks it and tells the model to pipe through sed to redact. The sentinel constant is split in the hook source so the script itself doesn't trigger the bug. Doesn't cover Bash tool calls (can't reliably parse shell commands for file paths).

PostToolUse hook — circuit breaker. Reads the session transcript JSONL, tracks cache_read vs cache_create per turn. If cache_read drops >95% from previous turn with >20K cache_create, that's a cold turn. 6 consecutive cold turns with short gaps (<5min) = permanently broken session → SIGTERM. Long gaps (>5min) are classified as TTL expiry and don't count — those recover on their own.

Has CACHE_BREAKER_KILL=1 env to opt into kill mode (default is warn-only), DRY_RUN=1 to log without killing, and a --scan flag to audit existing transcripts.

Gap neither hook covers: if the model writes the literal sentinel in a response (like when discussing this bug), it's in the conversation and the cache won't recover. Hooks can't filter assistant output. I added a line to CLAUDE.md telling it to never write the literal and use the redacted form instead.

npm binary is still the cleanest fix — no sentinel replacement code at all. These hooks are for people sticking with native.

jmarianski · 3 months ago

@kyzzen Where are the hooks? They sound like something i worked on yesterday :D Although I opted for just the "deny" after post tool use.

jmarianski · 3 months ago

Anyway, here is my plugin: cache-catcher in hooker marketplace (hooker is a universal plugin similar to hookify for more deeply ingrained knowledge on how to make cool hooks).

Cache catcher works in similar way: by default it should warn you if cache is malformed on previous request (parsing jsonl files) and if so, notifies you (and agent). You can easily configure it to instead prevent further execution if it happens. Worked like a charm. Although the only issue I've had were faulty resumes, so not much of a gain.

EDIT: If you prefer to use github: https://github.com/jmarianski/hooker

kyzzen · 3 months ago

@jmarianski thanks for cache-catcher — reading your implementation helped. Especially the PreToolUse + deny approach instead of PostToolUse + kill. Claude sees why the tool was blocked and can inform the user instead of the process just dying.

I built my own detection hook (not sharing — I'm not a dev and it's definitely vibe coded slop) but wanted to share one finding from the data:

TTL rebuild after re-login — same session ID, but cache expired while logged out. Full context (124k tokens) rebuilt from scratch: cache_read: 0 on turn 19 of an existing session. The cumulative counters also reset on re-login, so it looks like a new session in the logs even though it isn't. cache_read drops to exactly 12,705 — that seems to be the platform-level system cache that survives independent of the ephemeral session cache. Every cold turn reads 12,705 and rewrites everything above it.

This might be obvious, but easy to overlook if you're running multiple accounts with multiple 100k+ context sessions. One 250k cache_create bumped 3% of my usage by itself.

jmarianski · 3 months ago

@kyzzen And do you think my code isn't vibe sloped? Every tool created within half a year is vibed. Please share. Spread awareness. I'll be off for Easter so someone's gotta do it :)

junaidtitan · 3 months ago

History invalidation is often caused by orphaned progress entries and broken parentUuid chains. When progress ticks accumulate faster than content messages, the chain walker can follow the wrong branch on reload.

Cozempic v1.4.1 fixes this at the source: progress-collapse removes all progress tick messages, and _relink_parent_chain() automatically repairs parentUuid + logicalParentUuid links when messages are removed. The doctor command can diagnose these chain corruption issues before they cause data loss.

pip install cozempic && cozempic init

If you're seeing invalidated history, try cozempic doctor first — it'll tell you exactly what's broken. Feedback welcome on whether the chain repair fixes the reload issue for you.

yucizhou · 3 months ago

This has allegedly been fixed in 2.1.90 -- anyone seeing an improvement?

arizonawayfarer · 3 months ago

@yucizhou no

joseph-holland · 3 months ago
This has allegedly been fixed in 2.1.90 -- anyone seeing an improvement?

The resume bug is different than this one. Don't think this is fixed yet either.

al3rez · 3 months ago
This has allegedly been fixed in 2.1.90 -- anyone seeing an improvement?

just /comapct or /clear often and use stable chanenl for claude it's better now and use clauditor to check the session turns

patrikmanii · 3 months ago

Haven't checked if it was fixed yet, as I was off ystd, interestingly enough Claude offered 17 euro's free credits when I went to verify my usage.

krakz999 · 3 months ago

Is there an official channel where they track the status of this issue?

I'm pretty much afraid to use CC now as I don't want to blow up my usage just because it isn't working correctly...

patrikmanii · 3 months ago

From what I know, no. And the Claude status does not signal any errors.

mmircoli-nexapp · 3 months ago

any news?

FlorianBruniaux · 3 months ago

Anthropic team ? any news ? TBH this is not acceptable. I explode my 20x 5h range in 30/45 min max ...

joseph-holland · 3 months ago
Anthropic team ? any news ? TBH this is not acceptable. I explode my 20x 5h range in 30/45 min max ...

The workaround to use the npm version is the way for now. I was blowing through my limits super quickly until I went back to the npm claude-code.

I agree that it's not acceptable. I find the lack of comms worse though.

UpayanGhosh · 3 months ago

I'm currently using npm and also rolled back to version 2.1.68 for now. Way better limits ig. Is there any more modern stable version after this?

michabbb · 3 months ago

let's bet what anthropic will respond (if they respond):

  • there is no problem ❤️
  • skill issue 🚀
  • imagination 👀
  • admission that something is off 🎉

let’s place bets, just vote with the emoji for what you think will happen 🙈

kyzzen · 3 months ago

Ran the cache test and it seems that it's fixed in the last 3 versions.

<img width="462" height="272" alt="Image" src="https://github.com/user-attachments/assets/d3e74761-441a-415e-98e4-fc5a14a95eb1" />

I switched to the latest version as well and the usage seems normal so far. I still won't /resume or continue a session that's been 1h+ idle (I'm looking into auto-running at SessionStart a /loop 45m keepalive)

FlorianBruniaux · 3 months ago

@kyzzen yes i've the same feeling

[Cache bugs CC#40524] Updated audit skill

Good news: Anthropic appears to have fixed two of the three cache bugs that were inflating API costs — though this is still worth confirming on your end.

  • Bug 1 (standalone binary, cch= sentinel) → reportedly fixed in v2.1.91
  • Bug 2 (--resume/--continue, deferred tools delta) → reportedly fixed in v2.1.90
  • Bug 3 (per-session attribution header) → still active, workaround via CLAUDE_CODE_ATTRIBUTION_HEADER=false in settings.json

I also integrated community findings from ArkNill covering two additional behaviors: silent microcompact (tool results replaced with [Old tool result content cleared] before being sent to the API) and the 200K chars aggregate cap on built-in tool results.

If you ran /check-cache-bugs in March, run it again — the skill is updated with the new version thresholds. If you're on v2.1.92 with the env var already set, you should be clean, but re-running the audit is the safest way to confirm.

Updated skill: https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/examples/commands/check-cache-bugs.md

Used this analysis: https://github.com/ArkNill/claude-code-cache-analysis to complete my skill

UpayanGhosh · 3 months ago
@kyzzen yes i've the same feeling [Cache bugs CC#40524] Updated audit skill Good news: Anthropic appears to have fixed two of the three cache bugs that were inflating API costs — though this is still worth confirming on your end. Bug 1 (standalone binary, cch= sentinel) → reportedly fixed in v2.1.91 Bug 2 (--resume/--continue, deferred tools delta) → reportedly fixed in v2.1.90 * Bug 3 (per-session attribution header) → still active, workaround via CLAUDE_CODE_ATTRIBUTION_HEADER=false in settings.json I also integrated community findings from ArkNill covering two additional behaviors: silent microcompact (tool results replaced with [Old tool result content cleared] before being sent to the API) and the 200K chars aggregate cap on built-in tool results. If you ran /check-cache-bugs in March, run it again — the skill is updated with the new version thresholds. If you're on v2.1.92 with the env var already set, you should be clean, but re-running the audit is the safest way to confirm. Updated skill: https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main/examples/commands/check-cache-bugs.md Used this analysis: https://github.com/ArkNill/claude-code-cache-analysis to complete my skill

So do you recommend me to upgrade my npm claude version to the latest which is 2.1.92?

kyzzen · 3 months ago
So do you recommend me to upgrade my npm claude version to the latest which is 2.1.92?

There weren't any cache issues (at least from my tests) while running it through npm/npx/brew.

UpayanGhosh · 3 months ago
> So do you recommend me to upgrade my npm claude version to the latest which is 2.1.92? There weren't any cache issues (at least from my tests) while running it through npm/npx/brew.

Ok so the main issues were basically on the native installer binray? So should I update like which version are your using now? I'm on 5x Max plan BTW.

kyzzen · 3 months ago

From my tests, the cache bug is fixed — any installation method (native binary, npm, brew) works as expected on recent versions. I'm on 2.1.92.

On the 1h idle cache expiry — I mentioned I was looking into auto-running /loop 45m keepalive at session start. Got it working. Claude Code supports an initialPrompt field in agent frontmatter that auto-submits as the first user turn (slash commands are processed). Combined with the agent setting in settings.json, every new session auto-creates a session-scoped cron that fires a keepalive prompt every 30 minutes, refreshing the cache TTL.

Setup:

~/.claude/agents/keepalive.md:

---
name: keepalive
description: Auto-starts a cache keepalive cron at session start
initialPrompt: "CronCreate: cron=*/30 * * * *, prompt=keepalive. No explanation needed."
---

~/.claude/settings.json:

{
  "agent": "keepalive"
}

Each session gets its own independent cron (session-scoped, no lock contention across parallel sessions). Cost is ~260 output tokens at session start.

Important: keep the markdown body empty. The agent body (text after frontmatter ---) becomes the system prompt and replaces the entire default Claude Code system prompt — including behavioral sections like tool usage, safety, output style, etc. If you put descriptive text in the body, Claude loses all its standard instructions. The description frontmatter field is where the agent description belongs; the body should stay empty for a transparent wrapper like this.

Renvect · 3 months ago

Iv been diging on my own too. For some reason despite that Spring Break promotion that have been ran is over it still applys it. I noticed that usages escalate drasticaly out of promotion times htat where been set in the past.

This applys mostly to GUI based Claude Code within VS code hyperinflating during times when it was been set to be more expensive. It may be related to underlying bug. Ill dig on my side deeper around this what i observed last 4 days. I literaly tested it today 13:00 my time and it escalted. Beafore that time i have been playing around it was consuming even less.

I made my own plugin to track and hunt down costs and sessions issues and it shows me drastic rampage in usage at times designated to be more expensive.

kyzzen · 3 months ago
Iv been diging on my own too. For some reason despite that Spring Break promotion that have been ran is over it still applys it. I noticed that usages escalate drasticaly out of promotion times htat where been set in the past.

In my experience, in the peak hours the cost is the same but the 5h window is halved.

UpayanGhosh · 3 months ago

So guys do you recommend upgrading to hotfix 92?

UpayanGhosh · 3 months ago

Also what are the current active bugs for which we are still having this increased token usage?

dltn contributor · 3 months ago

Thank you for the detailed report! A fix has shipped in 2.1.90

cafferychen777 · 3 months ago

Can someone tell me if the issue has actually been resolved? I'm starting to distrust the official website and don't dare to update. I think this problem has been going on for at least two weeks. For the past two weeks, I've been in a state of overconsumption. I'm a 20x user, and my refresh time is Thursday and Friday, which usually allows me to last until then. But these past two weeks, I've basically run out of quota by Tuesday. If I don't control myself.

michabbb · 3 months ago
Can someone tell me if the issue has actually been resolved?

@cafferychen777

You have read the last message, you have, right? Sure your have, you must have..... Have you?

cafferychen777 · 3 months ago
> Can someone tell me if the issue has actually been resolved? @cafferychen777 You have read the last message, you have, right? Sure your have, you must have..... Have you?

I no longer trust official announcements. The last few versions of Claude Code have been terrible. They claimed to have fixed an issue, only for it to reappear and worsen just hours after release, forcing them to urgently push another update on the same day.

physics91 · 3 months ago

@cafferychen777 agree

krakz999 · 3 months ago

I wish these subscriptions would have a transparent pricing... especially now since subscription users seem to lose trust.

cnighswonger · 3 months ago

The official fix in 2.1.90 improved things but did not fully resolve the underlying causes. We've been tracking this extensively in #42052 with contributions from @jmarianski, @VictorSun92, and others, and can confirm the regression persists through v2.1.90–2.1.92. This is independently corroborated by #43657 (confirmed regression with proxy dumps on v2.1.92), #44045 (SDK-level reproduction with exact token measurements), and #43044 (resume loading 0% context on v2.1.91).

What 2.1.90 fixed vs. what it didn't

The 2.1.90 fix addressed some of the attachment block placement, but blocks still partially scatter on resume through at least v2.1.92. @VictorSun92 identified this on v2.1.90 via our fetch interceptor debug logging. #43657 and #44045 independently confirm the same pattern on v2.1.92 with MITM proxy captures and SDK token counts:

Fresh:  messages[0]: [deferred_tools, MCP, skills, context, user_text]
Resume: messages[0]: [deferred_tools, MCP, context, user_text]   ← skills missing
        messages[2]: [skills, resume_text]                        ← scattered here

This breaks the cache prefix match on every subsequent turn, producing the exact pattern from the original report: healthy cache_read → sudden revert to massive cache_creation with only ~11k cache_read (system prompt).

Root causes (three, not one)

  1. Partial block scatter — Relocatable attachment blocks drift to later messages during conversation. The original root cause analysis by @jmarianski (MITM proxy capture + Ghidra reverse engineering in #34629) identified the messages[0] asymmetry. @VictorSun92 wrote the first patch — a monkey-patch on v2.1.88 that relocated attachment messages to match fresh session layout. The 2.1.90 fix has a remaining detection gap: it bails early if it sees some relocatable blocks in messages[0], missing the case where others have scattered elsewhere. @VictorSun92 identified this partial scatter on 2.1.90 and contributed three fixes to our interceptor: a forward-scan for scattered blocks outside messages[0], corrected block ordering to match fresh session layout ([deferred, mcp, skills, hooks]), and tighter block matchers to prevent false positives from quoted content.
  1. Fingerprint instability — The cc_version hash in the attribution header is computed from content that includes meta/attachment blocks. When these blocks shift between turns, the fingerprint changes → system prompt changes → cache bust.
  1. Non-deterministic tool schema ordering — Tool definitions can arrive in different orders between turns, invalidating the cache key even when the actual tool set hasn't changed.

Community workaround

Building on @VictorSun92's original patch approach and @jmarianski's root cause analysis, we developed a Node.js fetch interceptor (posted in #42052) that addresses all three causes at the request layer. On a long-running agent session it achieved 98.2% cache hit ratio across 7,094 API calls — with busts occurring exclusively on TTL expiry, never from within-session instability. The interceptor, installation guide, and @jmarianski's multi-mode test script (which we extended) are all in the #42052 thread.

Additional finding: overage TTL downgrade

Also documented in #42052 — exceeding 100% of the 5-hour quota window triggers a server-enforced TTL downgrade from 1h to 5m. This creates a runaway feedback loop: overage → 5m TTL → frequent cache rebuilds → more tokens burned → deeper into overage. This isn't a bug per se (it's a consequence of overage billing using the standard API path which only supports 5m TTL), but it dramatically amplifies the cost impact of any cache instability.

Additional finding: trailing newline on re-normalization

#43657 also identified a +1 byte discrepancy in text blocks on resume: the companion system-reminder grows from 533 → 534 chars due to an extra \n appended during re-normalization. This is a secondary cache invalidation source — even if block placement were fixed, the byte difference would still bust the prefix.

Summary

The issue should be reopened or a follow-up filed. The 2.1.90 fix was a step in the right direction but the partial scatter (identified by @VictorSun92, independently confirmed in #43657 and #44045 on v2.1.92), fingerprint instability, schema ordering, and re-normalization byte drift all persist. Happy to share debug logs or interceptor source if it helps the team reproduce.

cnighswonger · 3 months ago

We are working to roll our interceptor as an npm (and publish the source on github). I'll follow up once it is available in case anyone is interested.

cnighswonger · 3 months ago

Here it is if anyone is interested. YMMV. As always: Absolutely no warranty of any kind attached to this.

https://www.npmjs.com/package/claude-code-cache-fix

https://github.com/cnighswonger/claude-code-cache-fix

cafferychen777 · 3 months ago
Here it is if anyone is interested. YMMV. As always: Absolutely no warranty of any kind attached to this. https://www.npmjs.com/package/claude-code-cache-fix https://github.com/cnighswonger/claude-code-cache-fix

Thank you for saving my quota. I will definitely try this!

Renvect · 3 months ago
Here it is if anyone is interested. YMMV. As always: Absolutely no warranty of any kind attached to this. https://www.npmjs.com/package/claude-code-cache-fix https://github.com/cnighswonger/claude-code-cache-fix

Tested it against standard claude vs fix you mention here and i wraped and ran it. Definetly reduces usage on same exact task. Results show 41% drop in my tracking plugin. Usage limit in claude code slows down.

cnighswonger · 3 months ago

Here's an(other) internal tool we find helps save $$ for long running contexts due to cold-cache costs:

https://github.com/cnighswonger/claude-code-coffee

It should work without the Claude Code Cache fix, but is more accurate with that fix installed.

jmarianski · 3 months ago

I'm back from Easter break :D While I appreciate the sentiment of continuing the discussion here (and I enjoy the claude code coffee break skill as an idea), perhaps we could continue in other issue? Perhaps something related to the /resume? I know the issue with --resume got closed cause it might be fixed there (cli has its own library, if I had known, I would have created a separate issue). There's obviously this issue: https://github.com/anthropics/claude-code/issues/42260 which should be easily fixable (as signature tokens shouldn't be sent to anthropic in the first place, and mechanism for removing "thinking" block are already present there), however I haven't subscribed to any issue that talks specifically about just /resume. Do you have any worth looking at links?

I've tested and see that cch=00000 issue has been fixed, apparently in 2.1.90 (check if you're not on stable version, if so, you'll be on around ~2.1.82).

Renvect · 3 months ago
> Here it is if anyone is interested. YMMV. As always: Absolutely no warranty of any kind attached to this. > https://www.npmjs.com/package/claude-code-cache-fix > https://github.com/cnighswonger/claude-code-cache-fix Tested it against standard claude vs fix you mention here and i wraped and ran it. Definetly reduces usage on same exact task. Results show 41% drop in my tracking plugin. Usage limit in claude code slows down.

Conducted more test against Claude code in VS extension vs wrapped fix. Claude code within gui definitely rampages over usage unlike up linked fix.

I am observing drastic differences with tracker. I will tomorrow have good dose of data on sessions I tracked in various env and will link up here as well as push out this plugin to intercept and hook in for others to check.

Renvect · 3 months ago

<img width="1231" height="906" alt="Image" src="https://github.com/user-attachments/assets/02d28b62-bd23-4ae4-adff-b1c9f93d4a9e" />

The interceptor I built is showing that an image pasted into Claude Code is being carried four times and automatically injected into the conversation, causing usage spikes. It is stored permanently in the conversation history and continues to be carried forward when the session or conversation is resumed.

The image persists in memory and keeps being reuse. In my session, this accumulated over six subsequent resumes before stopping.

Further logging shows that around 122k tokens are automatically inserted when a conversation is resumed within the current session. I am now examining the logs in more detail and attempting to reposition the interceptor to strip this data out and observe further results.

Striping is working, now looking more deeply into cause and where it is occuring

cnighswonger · 3 months ago

@Renvect — your image duplication finding is significant. We dug into the Claude Code source to understand the mechanics, and the cost implications are substantial.

How image persistence works in CC (confirmed from source)

When the Read tool opens an image file (PNG, JPG, etc.), it encodes the image as a base64 content block in the tool result (src/tools/FileReadTool/FileReadTool.ts:784-799). That tool result — base64 data and all — stays in the conversation messages array and is sent to the API on every subsequent turn as part of conversation history (src/services/api/claude.ts:1266-1315).

Images are only removed when:

  1. /compact is run (replaces images with [image] text placeholder — src/services/compact/compact.ts:145-179)
  2. The 100-media-item API limit is hit, at which point the oldest are silently dropped (src/services/api/claude.ts:956-1015)
  3. API-side context management clears old tool results

There is no automatic summarization — images are either fully present (base64) or fully gone.

Cost impact of image carry-forward

The token cost of a base64 image is approximately base64_bytes × 0.125. For typical images:

| Image size (base64) | Tokens per turn | Cost per turn (Opus $5/MTok) |
|---|---|---|
| 200KB (small screenshot) | ~25,000 | $0.125 |
| 500KB (typical) | ~62,500 | $0.31 |
| 1MB (large) | ~125,000 | $0.63 |
| 5MB (API max) | ~625,000 | $3.13 |

This compounds: 3 accumulated images at 500KB each = ~187,500 extra tokens on every turn for the rest of the session. Over 10 turns, that's ~1.9M wasted input tokens ($9.38 on Opus). On the 5m TTL tier, those images also trigger cache_creation rebuilds every 5 minutes.

Your observation of ~122k tokens auto-injected on resume is consistent with 1-2 accumulated images being replayed from conversation history.

Mitigation we're building

We maintain a fetch interceptor for cache regression fixes. We're adding image stripping to it — configurable via CACHE_FIX_IMAGE_KEEP_LAST=N to strip base64 image blocks from tool results older than N turns, replacing them with a text placeholder. Images read from /tmp or elsewhere are still on disk for re-reading if needed; they just don't need to ride along in every API call.

Renvect · 3 months ago

Went further into it. I got to pont of system prompt where it may somehow connect cross projects it was opened in. Literally lists them and builds it up prompt further. Wondering now if this image duplication is coming out of memory of what projects it worked on and somehow loads images of all this workspaces. Tracing further. Project will be up on git repo around 7 UTC

cnighswonger · 3 months ago

@Renvect — interesting that you're seeing cross-project contamination. Quick question: from your earlier comment it looks like you're using the VS Code extension ("GUI based Claude Code within VS code"). Can you confirm?

The reason we ask: the fetch interceptor approach we're using only works with the CLI client (claude via Node.js + NODE_OPTIONS="--import"). It won't apply to the VS Code extension, desktop app, or web client, which have their own process lifecycles. The underlying image persistence bug is the same across all clients, but the cross-project bleed you're tracing may be specific to how the VS Code extension assembles context across workspace folders — and if those clients share memories, that could explain the 4x duplication you observed.

Looking forward to seeing your repo when it's up.

Renvect · 3 months ago

Further FS reads and its analysis might be another point of cause where some plugin is hooking into CLI and injecting. Ill look into that next as it shows me spikes for Marketplace and Plugin.

Also to respond on question:

interesting that you're seeing cross-project contamination. Quick question: from your earlier comment it looks like you're using the VS Code extension ("GUI based Claude Code within VS code"). Can you confirm?

I have moved away from VS code and are working on terminal based claude code for now, i will next test all of this for VS code extension.

<img width="1241" height="897" alt="Image" src="https://github.com/user-attachments/assets/1ffc70c4-ef11-439f-a869-68ceaf2ae3f4" />

Also here is part of system prompt that came out during diging process. its just small chunk and may be relative to cross project memmory being somehow connected together.

<img width="1252" height="622" alt="Image" src="https://github.com/user-attachments/assets/9ddff1cd-0692-441e-8fd7-fa64d5de9973" />

cnighswonger · 3 months ago

@Renvect — we dug into the CC source to understand what's driving those "Additional working directories" entries in your system prompt. Here's what we found:

Where they come from

Additional working directories are populated from two sources (src/utils/permissions/permissionSetup.ts:993-1025):

  1. The --add-dir CLI flag — and importantly, these are persisted to settings.json so they survive across sessions
  2. Automatic symlink detection if your PWD resolves to a different path

What each additional directory does

Every directory listed there can silently inject content into your session:

  • Plugins — CC reads .claude/settings.json from each additional directory and merges any enabled plugins into the session (src/utils/plugins/addDirPluginSettings.ts:34-71)
  • MCP servers.mcp.json files are discovered by walking parent directories upward from CWD (src/services/mcp/config.ts:888-960)
  • CLAUDE.md — if CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD is set (off by default), memory files from each directory are loaded into the system prompt

No limits enforced — there's no cap on the number of additional directories. Each one adds to system prompt size and potentially loads its own plugin/MCP configuration.

Relevance to the image issue

The additional directories themselves don't directly inject images into the system prompt. But if any of those directories configure MCP servers or plugins that handle images, the content those tools produce would persist in conversation history the same way we described above — base64 data riding along on every subsequent API call until compaction.

It's worth checking your settings.json (likely at ~/.claude/settings.json or the project-level equivalent) for a permissions.additionalDirectories array. If directories were added via --add-dir at some point, they'll still be there. Removing any you don't actively need for the current project would trim your system prompt.

Renvect · 3 months ago

Initial version and todays logs:

https://github.com/Renvect/X-Ray-Claude-Code-Interceptor/tree/main

Experimenting further with capture and detection capabilities.

npetrangelo · 3 months ago
## Full reverse engineering analysis of the sentinel replacement mechanism ### Summary Through MITM proxy capture + Ghidra reverse engineering of the Claude Code standalone binary (228MB ELF), I've identified the exact mechanism causing conversation history invalidation. The root cause is a native-layer sentinel replacement in Anthropic's custom Bun fork that rewrites the billing attribution header value on every API request. ### The Mechanism Located at VA 0x0374d610 in the standalone binary (v2.1.87) — inside the Zig HTTP header builder function (src/http.zig in Bun's source tree). This is the same function that builds Content-Length, User-Agent, Host, Accept-Encoding, etc. Trigger condition: The replacement fires when ALL of these are true: 1. Request contains anthropic-version HTTP header (Wyhash = 0x58e54d60e1462681, verified via Bun.hash()) 2. Request URL path contains /v1/messages 3. Request body contains the sentinel string cch=00000 (9 bytes) What it does: 1. Searches the serialized JSON request body for the first occurrence of cch=00000 2. Hashes the entire request body (using a hash function with fixed seeds: 0xcf3c9b5975c738f4, 0x310521a7efdb6e6d, 0x6e52736ac806831e, 0xd01af9b9421ab897) 3. Converts 5 nibbles of the hash to hex characters using SIMD instructions (pshufb/pblendvb) 4. Writes the 5 hex characters in-place into the body buffer, replacing 00000 Decompiled pseudocode (reconstructed from Ghidra): // Inside HTTP header builder — after iterating request headers if (has_anthropic_version_header) { // Check URL contains "/v1/messages" // Built on stack as u64 LE immediates: local_c0 = 0x7373656d2f31762f; // "/v1/mess" local_b8 = 0x73656761; // "ages" if (memmem(url_ptr, url_len, &local_c0, 12) != NULL) { // Search body for sentinel "cch=00000" // Also built on stack: local_c0 = 0x303030303d686363; // "cch=0000" LE local_b8 = 0x30; // "0" offset = memmem(body_ptr, body_len, &local_c0, 9); if (offset != NULL) { // Hash the entire body hash_state = init(seeds...); hash_update(hash_state, body_ptr, body_len); hash_value = hash_finalize(hash_state); // Convert 5 nibbles to hex (SIMD) hex_chars = simd_nibble_to_hex(hash_value); // Write in-place: body[offset+4..offset+9] = hex chars (uint32_t)(body_ptr + 4 + offset) = hex_chars[0..3]; (char)(body_ptr + 8 + offset) = hex_char_5th; } } } ### Key findings 1. Only the standalone binary does this — not the JS code Confirmed experimentally: Runtime Replacement active? Official standalone binary (228MB ELF) YES bun build --compile --bytecode cli.js (homebrew standalone) NO bun cli.js (standard Bun runtime) NO npx @anthropic-ai/claude-code (npm package) NO Same JavaScript code, same bytecode, same Bun standalone format — different behavior. The replacement is in Anthropic's custom Bun fork, compiled into the native Zig HTTP client. 2. Sentinel is hardcoded as stack immediates — invisible to string search The pattern cch=00000 is built on the stack using MOV imm64 instructions: MOV [rsp+X], 0x303030303d686363 ; "cch=0000" in little-endian MOV byte [rsp+X+8], 0x30 ; "0" — completing "cch=00000" This is why searching the binary for 63 63 68 3d (cch=) or 0x303030303d686363 as raw bytes in .text yields zero results — the bytes are part of instruction immediates, not contiguous string data. 3. Replacement value is a body hash, not random The replacement value is deterministic per request body — it's a hash of the entire body with fixed seeds. But since the body changes every request (new messages, timestamps), the hash value changes too, making it appear random. 4. Only the FIRST occurrence is replaced There is no loop — memmem finds the first match, replacement writes 5 bytes, and the function continues to build Content-Length headers. This means: If messages[] (which comes BEFORE system[] in the JSON body) contains cch=00000, that gets replaced instead of the billing header in system[0] The sentinel in system[0] survives — visible to the model as literal cch=00000 5. Header hashes identified The function uses Zig's std.hash.Wyhash (seed=0, confirmed via Bun.hash()) for case-insensitive header name comparison: Hash Header 0x58e54d60e1462681 anthropic-version — triggers replacement 0xba5173bf47c57684 connection 0x4cc2547449bddafc upgrade 0xfc8491525da4c9e3 content-length 0x308b6fc4af845531 transfer-encoding 0x2e6b2eda627d6669 user-agent 0x62b7d00b4bd17658 accept-encoding 0xfbb5acdf8484b6ef accept 0x80bd44e8947f37d2 host 0x3dfa2d55bad7bb18 if-modified-since 6. Sentinel history CC Version cch=00000 in billing header? 1.0.0 – 2.0.0 No billing header at all 2.1.0 – 2.1.34 Billing header exists, but no cch field 2.1.36 cch=00000 introduced 2.1.36 – 2.1.87 Present in all versions ### How this causes cache invalidation The billing header cch=00000 is placed in system[0] by the JS function DG$(). On the standalone binary, the native replacement changes 00000 to a body-hash value (e.g., a3f1b) before the request leaves the process. In normal sessions (no sentinel in conversation content): Only system[0] is affected. Since system[0] has cache_control: null (no caching), this doesn't break the cache prefix — system[2] (main prompt with cache_control: ephemeral) and messages[] remain stable. When conversation content contains cch=00000: This happens when: CLAUDE.md discusses the billing mechanism (our research notes) Read/Grep tool reads the JS bundle or binary containing the sentinel * User types the sentinel literally Since messages[] comes BEFORE system[] in the JSON body, the sentinel in messages is replaced FIRST. The actual billing header in system[0] keeps 00000. But the changed value in messages breaks the cache prefix from that point onward. ### Methodology 1. MITM proxy (mitm-addon.py via mitmproxy) capturing all API request/response payloads 2. Binary comparison: official standalone (228MB) vs homebrew bun build --compile (224MB) — same JS, different behavior 3. Ghidra 12.0.4 reverse engineering of the standalone ELF, identifying FUN_0374d610 as the HTTP header builder with injected replacement logic 4. radare2 disassembly for cross-referencing and function analysis 5. Bun.hash() to verify all header name hashes (Zig Wyhash, seed=0) 6. npm package analysis across versions 1.0.0 through 2.1.87, identifying cch=00000 introduction in v2.1.36 7. Controlled experiments: fresh session → resume → consecutive resumes with full payload diffing 8. 62 non-research project transcripts verified: zero accidental billing header leaks into messages ### Related This finding also explains the root cause of #34629 (cache regression on --print --resume since v2.1.69), where the deferred_tools_delta attachment introduced in v2.1.69 causes messages[0] to differ between fresh sessions and resumed sessions, independently breaking cache prefix matching.

So...does this mean open source Claude Code gets around this issue?

jmarianski · 3 months ago
So...does this mean open source Claude Code gets around this issue?

If you mean open code and other alternatives - yes. If you mean if we can find the bug in "open source" code - no. It was in the binary that came alongside js (that leaked).

github-actions[bot] · 3 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.