Emit --append-system-prompt as its own system block so a changed append does not invalidate the preset

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 0 comments · opened Sep 2, 2026

Summary

The CLI concatenates the claude_code preset and the caller's --append-system-prompt text into a single system content block, and places its cache_control mark at that block's end. Because a cache entry is keyed on a whole block, any change to the appended text invalidates the entry for the preset as well — so a caller whose appended content varies re-pays for the CLI's own system prompt every time.

Splitting the append into its own block, with a boundary in front of it, would confine that invalidation to the appended text.

Measured

Captured with a local pass-through proxy in front of the CLI (2.1.258, Agent SDK 0.3.146, claude-opus-5, reproduced on claude-haiku-4-5). Every row is a cache_control block in a body the CLI actually sent:

| block | size | mark |
| --- | --- | --- |
| system[0] | 74 chars | — |
| system[1] | 94 chars | 1h |
| system[2] | 27,723 chars — preset + append concatenated onto its tail | 1h |

The append began at char 27,642 of 27,723, i.e. inside system[2] rather than as a block of its own.

Mark budget across rounds:

  • Round 1 — three CLI marks: system[1], system[2], last user content block.
  • Rounds 2+ — four: system[1], system[2], tool_use, tool_result. The CLI drops its last-message mark when it adds the tool pair.

All marks carry ttl: 1h.

Why it costs

In a real workload the cached prefix is ~109k tokens, of which ~20k is appended content that changes occasionally (a set of instruction files that grows as a session progresses) and ~79k is the preset plus static appended material. Because it is all one block, changing the 20k rewrites the 79k with it, at cache-write price.

That inverts the economics of appending anything that is not perfectly static. In our measurements it takes roughly four unchanged turns to pay back one changed turn — so a caller whose appended content changes even occasionally is close to break-even on caching it at all, purely because of where the boundary falls.

Request

Emit the appended system prompt as its own content block:

system[2]   preset          ← mark        ← boundary here
system[3]   append          ← mark

Then a change to the append invalidates only the append; the lookback finds the entry at the end of system[2] and the preset is read from cache.

The budget objection, addressed

This is a third system mark, and on a round carrying a tool call the CLI is already at four of the API's four. The request therefore includes re-budgeting: drop the mark on system[1] — 94 characters, which caches nothing worth having — and spend it on the preset/append boundary.

That is the same kind of budgeting the CLI already performs internally: it yields its last-message mark the moment it needs the tool_use/tool_result pair, as the capture above shows.

Related but distinct

anthropics/claude-agent-sdk-typescript#432 asks for caller-supplied marks in streamed input to be counted against the budget. That would solve a caller's problem by handing it a mark to manage. This asks for the block boundary instead, which needs no mark from the caller at all and benefits everyone using --append-system-prompt with content that is not byte-static — a larger group than those placing their own marks.

#87487 (suppressing the daily currentDate injection) is the same family: both are about what a caller can keep stable inside a marked block.

View original on GitHub ↗