anthropics / anthropics/claude-code

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

Ouverte
#89,651 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub
area:core area:cost area:hooks bug has repro regression
Langage dominant
Python
Étoiles
145k
Forks
23.1k
Métriques de merge des PR
Métriques de PR en attente

Description

### Preflight Checklist

- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) 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](https://github.com/anthropics/claude-agent-sdk-python/issues/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

```shell
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:

Run:

```bash
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](https://github.com/leoavelino/share_files/blob/main/002_claude_headless_cache_regression_2/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:

```text
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`.

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Piste de recherche

Start with the standalone repro_cache_regression.py script and run it with the 2.1.236 and 2.1.237 commands to compare captured /v1/messages requests. Trace the claude -p request serialization that creates the trailing system message; done means hook and reminder context uses the block-array form with cache_control and the reproduced input-token regression is gone.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
node.js, python
Domaine
api, cli, performance
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Plutôt claire
Accessibilité débutants
45/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.