Preflight Checklist
What's Wrong?
When using --resume to continue a session, the prompt cache is broken on every turn. Content that should be a cache hit is re-created instead, leading to massively increased session usage.
In particular, the cause is twofold:
Issue 1: Skill listing block migrates between messages on resume
A <system-reminder> block listing available skills (~1501 chars) is injected into the first user message on each turn. This block is not persisted in the session JSONL. On the initial turn it appears in messages[0]. On resume, the persisted messages are replayed without it, and a fresh copy is injected into the new user message instead.
This changes the block structure of messages[0], which invalidates the cache prefix for everything after it.
Observed in proxy dump — Turn 1 messages[0] (4 content blocks):
block[0]: 533 chars — companion system-reminder
block[1]: 1501 chars — skill listing system-reminder ← PRESENT HERE
block[2]: 7801 chars — project context system-reminder
block[3]: 4 chars — user text ("test"), cache_control: ephemeral 1h
Observed in proxy dump — Turn 2 messages[0] (3 content blocks):
block[0]: 534 chars — companion system-reminder (also 1 byte longer, see Issue 2)
block[1]: 7801 chars — project context system-reminder
block[2]: 4 chars — user text ("test"), NO cache_control
Turn 2 messages[2] (new user message, 2 content blocks):
block[0]: 1501 chars — skill listing system-reminder ← MOVED HERE
block[1]: 10 chars — user text ("test again"), cache_control: ephemeral 1h
The skill listing block moved from messages[0] (turn 1) to messages[2] (turn 2). Since the API caches by exact prefix match, this structural difference at the start of the messages array means the session-specific content can never be a cache hit on resume.
Issue 2: Extra newline appended to text blocks on re-normalization
When replayed messages are re-normalized on resume, an extra \n is appended to certain text blocks at merge boundaries. This is visible in the companion block:
Turn 1: 533 chars, ends with "</system-reminder>"
Turn 2: 534 chars, ends with "</system-reminder>\n"
This 1-byte difference independently invalidates the prefix cache, even if Issue 1 were fixed. The extra newline accumulates on each resume — a third resume would add another \n.
Expected behavior
Both issues are independently sufficient to break the cache. When both are corrected, resumed turns achieve ~100% cache hit:
turn hit% cached new_cache uncached total ctx% message
---- ---- ------- --------- -------- ------ ---- -------
1 68% 11271 5277 3 16551 2% test
2 100% 16548 45 3 16596 2% test again
3 100% 16593 44 3 16640 2% one more test
4 100% 16637 39 3 16679 2% test
HEALTHY: steady-state avg hit=100%, avg new cache=43
New cache creation drops from ~2850 to ~43 tokens/turn (just the new user message content).
What Should Happen?
claude --resume -p with a provided session, used within the cache time limit, should not invalidate prior session cache, nor introduce new tokens. In other words, --resume should be idempotent and --resume -p should be functionally identical to sending a new interactive turn, i.e. not generating unnecessary caching burden.
Error Messages/Logs
Steps to Reproduce
First, initialize a local HTTP reverse proxy intercepting API requests to api.anthropic.com, capturing full request payloads across a 4-turn --resume sequence.
# Turn 1: start session
claude -p --output-format stream-json <<< "test"
# → session_id: <id>
# Turns 2–4: resume
claude -p --output-format stream-json --resume <id> <<< "test again"
claude -p --output-format stream-json --resume <id> <<< "one more test"
claude -p --output-format stream-json --resume <id> <<< "test"
Observed cache behavior
turn hit% cached new_cache uncached total ctx% message
---- ---- ------- --------- -------- ------ ---- -------
1 0% 0 21220 3 21223 2% test
2 87% 18416 2828 3 21247 2% test again
3 87% 18416 2847 3 21266 2% one more test
4 87% 18416 2866 3 21285 2% test
UNHEALTHY: steady-state avg hit=87%, avg new cache=2847
Turns 2–4 should show ~100% cache hit with <100 tokens of new cache creation (just the new user message). Instead, ~2850 tokens are re-created on every resumed turn.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.71
Claude Code Version
2.1.92
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Non-interactive/CI environment
Additional Information
#42338 contained extensive documentation of these and other issues, including minified source patches, but was closed prior to 2.1.92 launching.
Preflight Checklist
What's Wrong?
When using
--resumeto continue a session, the prompt cache is broken on every turn. Content that should be a cache hit is re-created instead, leading to massively increased session usage.In particular, the cause is twofold:
Issue 1: Skill listing block migrates between messages on resume
A
<system-reminder>block listing available skills (~1501 chars) is injected into the first user message on each turn. This block is not persisted in the session JSONL. On the initial turn it appears inmessages[0]. On resume, the persisted messages are replayed without it, and a fresh copy is injected into the new user message instead.This changes the block structure of
messages[0], which invalidates the cache prefix for everything after it.Observed in proxy dump — Turn 1
messages[0](4 content blocks):Observed in proxy dump — Turn 2
messages[0](3 content blocks):Turn 2
messages[2](new user message, 2 content blocks):The skill listing block moved from
messages[0](turn 1) tomessages[2](turn 2). Since the API caches by exact prefix match, this structural difference at the start of the messages array means the session-specific content can never be a cache hit on resume.Issue 2: Extra newline appended to text blocks on re-normalization
When replayed messages are re-normalized on resume, an extra
\nis appended to certain text blocks at merge boundaries. This is visible in the companion block:This 1-byte difference independently invalidates the prefix cache, even if Issue 1 were fixed. The extra newline accumulates on each resume — a third resume would add another
\n.Expected behavior
Both issues are independently sufficient to break the cache. When both are corrected, resumed turns achieve ~100% cache hit:
New cache creation drops from ~2850 to ~43 tokens/turn (just the new user message content).
What Should Happen?
claude --resume -pwith a provided session, used within the cache time limit, should not invalidate prior session cache, nor introduce new tokens. In other words,--resumeshould be idempotent and--resume -pshould be functionally identical to sending a new interactive turn, i.e. not generating unnecessary caching burden.Error Messages/Logs
Steps to Reproduce
First, initialize a local HTTP reverse proxy intercepting API requests to
api.anthropic.com, capturing full request payloads across a 4-turn--resumesequence.Observed cache behavior
Turns 2–4 should show ~100% cache hit with <100 tokens of new cache creation (just the new user message). Instead, ~2850 tokens are re-created on every resumed turn.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.71
Claude Code Version
2.1.92
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Non-interactive/CI environment
Additional Information
#42338 contained extensive documentation of these and other issues, including minified source patches, but was closed prior to 2.1.92 launching.