[BUG] A background auto-update invalidates every existing session's prompt cache: the next --resume re-caches the entire context

Status Open
Reported on v2.1.228
Maintainer reply None cached
Activity 1 comment · opened Aug 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code auto-updates in the background while sessions are running. The running process keeps the build it started with, so nothing breaks at the time. But the new build ships a different system prompt and different tool descriptions, and those blocks sit ahead of the entire conversation in the request. The next time any pre-update session is resumed, the new binary assembles a prefix that diverges within the first ~22k tokens and the whole conversation is re-created as cache writes.

The cost is proportional to the size of the session being reopened, and is paid for content that had already been sent and was still sitting in a live cache entry. In the case below, one sentence of typed input cost ~3% of a weekly subscription cap. There is no warning.

This is distinct from #78720. That issue's trigger is per-machine dynamic content (git status) changing between turns, which requires a git repo and a working-tree change. This one requires neither — it fires on any session that spans an update, and the differing content is the binary's own prompt text.

Evidence

Session transcript, ~/.claude/projects/<project>/<session-id>.jsonl. Claude Code stamps the build on every record, so the split is visible in the user's own data:

2026-08-12T23Z  version 2.1.228  n=149 records   <- session, cache hitting every turn
2026-08-13T00Z  version 2.1.229  n=15  records   <- after resume

Usage across the pause:

23:51:00Z  last turn before pause    cache_read=890,802  cache_create=42       out=169
--- 34 minute pause ---
00:25:03Z  first turn after resume   cache_read=21,994   cache_create=794,306  out=2,931
00:26:40Z  next turn                 cache_read=816,300  cache_create=3,015    out=755

The third line matters: once the new build re-cached the context, caching resumed normally. The break is a one-time re-assembly, not ongoing instability.

Not expiry. Every write in the session used the 1-hour tier (cache_creation: {ephemeral_1h_input_tokens: N, ephemeral_5m_input_tokens: 0}) and the gap was 34 minutes. Not compaction — no compact-summary record anywhere in the transcript. Not a shrinking or growing suffix — the rebuilt prefix was 74,544 tokens smaller (816,300 against 890,844), so the content changed rather than expired.

The differing content is in the binary. Both builds were still on disk, so this is checkable directly:

for v in 2.1.228 2.1.229; do
  strings -n 60 ~/.local/share/claude/versions/$v |
    rg '^[A-Z][^{};=<>]*[a-z]{3}[^{};=<>]*\.$' | sort -u > $v.txt
done
diff 2.1.228.txt 2.1.229.txt

60 prose strings differ. Several are verbatim system-prompt and tool-description text:

< If the user explicitly asks you to remember something, save it immediately...
< Execute autonomously, minimize interruptions, prefer action over planning.
< Note: reading files, searching code, and other read-only operations do not require the classifier...
> Add task-specific tools to the same call when the task obviously needs them:
      read_console_messages / read_network_requests for debugging, form_input for forms...
> Either way, the page publishes with ordinary artifact visibility.

The fourth is the claude-in-chrome MCP instructions block, which is part of the system prompt; the last is the Artifact tool's description. Change either and every token after it is a cache miss.

Other inputs eliminated. Across the 34-minute pause: nothing git status-visible changed (the only modified files were gitignored), no commits, same branch, same cwd, same local date, and the session's SessionStart:resume hook output is appended at the tail of the conversation where it cannot invalidate the preceding 794k tokens. The build is the only input that differed.

Cost

Measured against the account's weekly_scoped meter (/api/oauth/usage, sampled every 15 minutes). It read 17% before the resume and 20% after. The resumed turn was 3 requests totalling 4,031 output tokens and 798,072 cache-creation tokens.

Calibrating that meter against 16 integer percent-crossings in the same window puts cache writes at ~0.087 output-token-equivalents (worst residual 0.24 points at that weight, against 2.24 for an output-only model):

output tokens alone   : 0.16 points
output + cache writes : 2.87 points     (95% of the charge is cache-write)

~3% of a weekly cap, for one sentence, on a session the user had already finished and came back to with an afterthought. Roughly one point per 294,000 tokens re-cached, so the charge scales with how much work is already in the session — the longer and more valuable the conversation, the more it costs to ask it one more question.

Worth noting that 2.1.229's own changelog contains:

Improved workflow fan-outs to stagger same-prefix sibling agents so subsequent agents read the cached prompt prefix instead of re-paying it

The same release optimises prefix reuse for fan-outs while its installation silently invalidates the prefix of every session already on disk.

What Should Happen?

In ascending order of effort:

  1. Warn before sending. On resume, compare the rebuilt prefix against the session's last-sent prefix and say what it will cost: "this session was created on 2.1.228 and will be rebuilt on 2.1.229 — resuming re-caches ~794k tokens. Continue, or start a fresh session?" The binary already carries the machinery: [PROMPT CACHE BREAK], [PROMPT CACHE] cache deletion applied, cache read: , and a comparator tracking systemHash, toolsHash, cacheControlHash, perBlockHashes, messageHashes with a buildDiffContent callback behind a cacheDiagnosis flag. It can already tell which block moved and what it will cost. It just doesn't say so before spending the user's cap. This alone would have prevented the charge above — the user would have opened a new session.
  1. Pin the prefix. When a session has a live cached prefix, reuse the prompt blocks it was built with for the remainder of that session's life rather than regenerating them from the current build.
  1. Don't swap the binary under live sessions. Defer the update until no session references the outgoing build, or keep the outgoing build resolvable for resumes of sessions that started on it. Both versions already remain on disk under ~/.local/share/claude/versions/.

Reproduction

  1. Start a session and grow the context (this one reached ~890k tokens).
  2. Let Claude Code auto-update in the background, or update it manually while the session process stays alive. Confirm with ~/.claude/.last-update-result.json.
  3. Stop for 10–50 minutes, inside the 1-hour cache TTL.
  4. Resume the session and ask a one-line question.
  5. Read cache_creation_input_tokens and cache_read_input_tokens on the first post-resume assistant record in the session transcript, and the version field on records either side of the pause.

Expected: a small write and a large cache_read_input_tokens.
Observed: cache_create=794,306, cache_read=21,994.

Related

  • #78720 — same collapse signature and the same ~21k floor, triggered by git status changing between headless -p --resume turns. I have added the interactive, clean-repo case there as corroboration. This issue is the binary-change trigger, which needs no repo and no working-tree change.
  • #67497 — the interactive --resume form, closed as stale on 2026-07-21. Itself a refile of #43657, which the inactivity bot closed as not_planned on 2026-06-11. Neither was fixed.
  • #66005 — same collapse via a different trigger (--resume dropping the session's --effort), still open.
  • #40524 — same token signature, closed.

Environment

Claude Code 2.1.229 (native install), macOS 27.0 arm64, model claude-fable-5, claude.ai subscription (1-hour cache TTL), interactive --resume, git repo with a clean working tree. Session transcript with per-record version stamps and full usage blocks available on request.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗