[BUG] Prompt-caching regression since 2.1.237: hook/reminder context is uncacheable again

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 0 comments · opened Aug 25, 2026

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 (@latest at the time of testing)
  • Verified good: 2.1.213, 2.1.221, 2.1.229, and 2.1.231 through 2.1.236
  • Verified bad: 2.1.237, 2.1.241, and 2.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.

View original on GitHub ↗