[BUG] TodoWrite re-stamps a front-of-context block on every update, invalidating the entire prompt cache (full ~900k-token rewrite per todo update)
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?
Every TodoWrite update invalidates the entire prompt cache and forces a full
cache_creation rewrite of the whole context (often 800k-900k tokens) at 1.25x,
for near-zero output.
The todo_reminder block lives near the front of the conversation (in the cached
prefix). On each update TodoWrite re-stamps that whole block, which changes the
cached-prefix fingerprint and invalidates everything after the system prompt -
so cache_read collapses to ~system size and the rest is re-written.
It fires once per item update, so a session where Claude works through a todo
list produces dozens of full rewrites in minutes. In my own logs this is the
single largest driver of 5-hour-quota spikes (~93% of my multi-million-token
spikes; one 14-minute window burned 16.7M tokens across 20 rewrites).
Critical detail: it busts even when the todo content is byte-IDENTICAL between
updates (same hash), so this is a caching bug, not the list legitimately changing.
What Should Happen?
Updating the todo list - especially with identical content - should NOT
invalidate the prompt cache. TodoWrite should patch the list in place (or keep
the block out of the cached prefix / append-only) so the cached prefix stays
byte-stable and subsequent turns get cache_read, not a full cache_creation.
Error Messages/Logs
One 14-minute window (deduped by message.id). Each TodoWrite update is followed
by a full rewrite; the todo content hash is identical on all 20:
19:23:50 TodoWrite update todo-hash=d75171 -> 19:24:06 cache_creation=770,583 cache_read=23,090
19:24:35 TodoWrite update todo-hash=d75171 -> 19:24:43 cache_creation=777,074 cache_read=23,090
19:25:12 TodoWrite update todo-hash=d75171 -> 19:25:20 cache_creation=783,119 cache_read=23,090
... 20 updates -> 20 full rewrites, 1:1, every hash = d75171 ...
19:37:30 TodoWrite update todo-hash=d75171 -> 19:37:40 cache_creation=897,340 cache_read=23,090
Sum: 16,686,355 tokens in 14 minutes. cache_read pinned at 23,090 (system only)
on all 20 = entire context after system rewritten each time.
Steps to Reproduce
- Start a session and build up a warm cache (a few hundred k of context - e.g.
read several files so the conversation grows).
- Ask Claude to create a todo list and work through several items, updating the
list (marking items in_progress/completed) as it goes.
- Watch usage / the prompt-cache fields per turn (/status, or the session JSONL
usage objects).
Observed: each TodoWrite update yields a turn with large cache_creation and
cache_read collapsed to ~system size (a full rewrite). 5-hour usage climbs fast
for near-zero output. Reproduces with identical todo content.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.173
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
SCALE - 16 spikes >=2M tokens over ~2 weeks (~101M tokens total):
15 of 16 are TodoWrite; the 16th was a normal idle-resume cold write.
By tokens, ~93% TodoWrite. By individual rewrites, 128/146 (88%) had a
TodoWrite update within 60s before; the biggest spikes are a clean 100%
(16.7M = 20/20, 9.2M = 11/11). Avg gap between rewrites 43-112s = active
work, NOT idle TTL expiry.
LIKELY ROOT CAUSE: #2250 (TodoWrite overwrites the entire list every update,
rather than patching one item) - that whole-list overwrite regenerates the
front block and changes the cached-prefix fingerprint. Mechanism overlaps
#27048 / #42338 (attachment/meta blocks busting the prefix on re-render).
This is NOT #40524 (that is the separate cch=00000 sentinel bug).
WORKAROUND (removed ~93% of my spikes) - in ~/.claude/settings.json:
"todoFeatureEnabled": false
- a PreToolUse deny hook on TodoWrite:
{
"matcher": "TodoWrite",
"hooks": [
{ "type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"TodoWrite disabled to prevent prompt-cache busts\"}}'" }
]
}
Track tasks in a markdown file instead - plain file edits don't inject a
cache-busting block.