[BUG] Prompt-caching regression since 2.1.237: hook/reminder context is uncacheable again
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?
Since Claude Code 2.1.237, headless (claude -p) sessions serialize SessionStart-hook additionalContext and the CLI's reminder blocks as a trailing role: "system" message whose content is a plain string.
The plain-string form cannot carry cache_control. Because this is the trailing message, the content lands after the final cache breakpoint and is billed again as uncached input_tokens at every session start.
This is a regression of the fix for anthropics/claude-agent-sdk-python#1120. The fix released in 2.1.212 encoded this message as a block array carrying cache_control, keeping the content inside the cached prefix. That behavior remained intact through 2.1.236; 2.1.237 changed it back to a plain string.
The regression appears to come from the change described in 2.1.237's changelog as:
Fixed prompt caching for sessions using an LLM gateway or custom base URL.
What Should Happen?
The trailing system message should carry cache_control using the block-array representation, as it did from 2.1.212 through 2.1.236. Alternatively, the hook/reminder content should remain within the cached prefix.
Error Messages/Logs
Captured request analysis for the last good and first bad releases:
== OLD: npx -y @anthropic-ai/claude-code@2.1.236
messages[1][0] role=system: text len=14832 [cache_control] <-- HOOK CONTEXT
VERDICT: hook context is INSIDE the final cache_control prefix (cacheable)
== NEW: npx -y @anthropic-ai/claude-code@2.1.237
messages[1] role=system (plain string): text len=14862 <-- HOOK CONTEXT
VERDICT: hook context is AFTER the final cache_control breakpoint (uncacheable)
The same plain-string form is still present in `2.1.245`, which was `@latest` at the time of testing.
Steps to Reproduce
the standalone reproduction requires no API key:
<https://github.com/leoavelino/share_files/tree/main/002_claude_headless_cache_regression_2>
Run:
python3 repro_cache_regression.py \
--old-bin "npx -y @anthropic-ai/claude-code@2.1.236" \
--new-bin "npx -y @anthropic-ai/claude-code@2.1.237"
The script creates a synthetic SessionStart hook, points ANTHROPIC_BASE_URL at a local capture sink, and records each CLI version's outgoing /v1/messages request before the intentionally failed API call.
The output shows that 2.1.236 places the hook/reminder context inside the final cache breakpoint, while 2.1.237 places the same context after it.
Full runs, including @latest, are available in repro.md.
Claude Model
None
Is this a regression?
Yes, this worked in a previous version
Last Working Version
S
Claude Code Version
2.1.245
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Version boundary
- Last good:
2.1.236 - First bad:
2.1.237 - Still affected:
2.1.245(@latestat the time of testing) - Verified good:
2.1.213,2.1.221,2.1.229, and2.1.231through2.1.236 - Verified bad:
2.1.237,2.1.241, and2.1.245
The issue reproduces with claude-sonnet-5 in print mode. The billing effect is visible across all model families in our environment.
Billing confirmation
Using the same protocol as the linked claude-agent-sdk-python#1120 issue, run two fresh claude -p sessions back-to-back so that the first warms the cache and the second measures steady state.
On the second run:
| Version | input_tokens |
|---|---:|
| 2.1.236 | 2 |
| 2.1.241 | approximately 4,500 |
For both versions:
input_tokens
+ cache_read_input_tokens
+ cache_creation_input_tokens
= 22,425
The prompt did not grow. The same tokens moved out of the cached buckets and into full-price input_tokens.