Skip to content

[BUG] -p --resume from a git repo: any git status change between turns invalidates the entire prompt cache (full-prefix re-creation) #78720

Closed as not planned

Description

@redletteroffice

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

The CLI embeds dynamic per-machine sections — including git status output — into the system prompt at every spawn. For headless spawn-per-turn integrations (claude -p --resume <id> launched fresh each turn by a host process), any change to the repo's status output between turns (a commit, a new untracked file, a deleted marker) changes the system prompt. The system prompt sits ahead of the entire conversation, so the whole cached prefix is invalidated: cache_read_input_tokens collapses to the ~21k shared-boilerplate floor and the full conversation history is re-created as cache_creation_input_tokens.

Measured across one workspace's full transcript history (~/.claude/projects/<workspace>/*.jsonl): 1,407 of 16,058 API calls (8.8%) show the full-collapse signature — 161,124,671 tokens re-created in collapse events across 183 sessions. This burns subscription usage-cap headroom and adds latency; for API-key users it re-bills entire conversation histories at cache-write rates.

This is not the tool-reordering resume bug (#42338 / #34629): it reproduces with zero tools, zero MCP servers, and a one-word reply, and it is deterministic on demand via git status churn alone.

What Should Happen?

A resumed turn should serve the prior conversation from cache: at call N, cache_read_input_tokens should equal the cumulative read + creation of every prior call (the healthy signature, which holds reliably from a non-repo cwd). A change in git status output between turns should not re-bill the entire conversation history — e.g., the dynamic sections could sit behind a cache breakpoint / after the stable prefix, or be frozen to session-start values for -p --resume continuations (the model already receives fresh state via tool use).

Error Messages/Logs

No error is emitted - the failure is silent re-billing, visible only in per-call usage.
Re-run on 2.1.212 (fresh minimal repo, untracked marker flipped between resumes;
per-call usage from --output-format json):

turn1  read=21,213  create=9,203            (session created)
turn2  read=21,213  create=9,516   expected read=30,416  -> FULL COLLAPSE (prior prefix re-created)
turn3  read=30,416  create=364     expected read=30,729  -> partial re-creation
turn4  read=30,729  create=109     healthy
turn5  read=30,780  create=108     healthy

Same-day control from a NON-repo cwd, identical churn in an --add-dir'd repo: 4/4 healthy exact-prefix reads.

Steps to Reproduce

  1. Control (non-repo cwd): in an empty non-repo directory run claude -p "Reply with exactly: ok" --output-format json, then 4–5 more turns with --resume <session_id>. Observe: every resume's cache_read_input_tokens exactly equals the prior cumulative read + creation. 0/5 collapses.
  2. Trigger (git repo cwd + churn): git init a minimal repo (one committed README), run the same turn-1 command from inside it, then the same --resume turns — but between each turn create or delete an untracked marker file (any change to git status output).
  3. Observe: resumes following a status change collapse — cache_read falls to the ~21k floor and cache_creation re-does approximately the entire prior cumulative total. 3/5 collapsed on 2.1.207; 1 full collapse + 1 partial re-creation across 4 churned resumes on 2.1.212.
  4. Collapses occur only on turns where the status output actually changed since the prior spawn — matching the intermittent ~9–15% rate observed in production traffic.

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

No response

Claude Code Version

2.1.212 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Non-interactive/CI environment

Additional Information

  • Auth/platform detail: first-party Anthropic backend via Claude Max subscription OAuth login — no API key, no gateway/proxy. Windows 11, native binary install (not npx).
  • Models: minimal repro on Haiku (model-independent, keeps repro cost trivial); the same collapse signature is observed with Opus across production traffic in the measured corpus.
  • --exclude-dynamic-system-prompt-sections makes it WORSE for this use-case: it moves the dynamic sections into the first user message, which regenerates on every resume — 5/5 collapses on the same repro.
  • Related but distinct: Session resume (--continue) invalidates entire prompt cache, causes massive rate limit consumption #42338 and [BUG] Prompt cache regression in --print --resume since v2.1.69(?): cache_read never grows, ~20x cost increase #34629 (resume cache invalidation attributed to deferred_tools_delta tool reordering, v2.1.69+). This mechanism needs no tools at all; the trigger is the git-status section of the system prompt.
  • Workaround verified locally: spawn the subprocess from a stable non-repo cwd (removes the git-status section entirely) with repo access preserved via --add-dir <repo> — 0/4 collapses with deliberate status churn in the add-dir'd repo. Only viable for sessions that don't require a repo cwd.

Activity

  1. jcll commented on Aug 13, 2026

    @jcll

    Corroborating this from a different trigger, and with a cost figure in subscription-cap units rather than raw tokens.

    Same signature, no git-status change, not headless. Ordinary interactive session, resumed after a 34-minute pause. Working tree clean, no commit inside the pause window, nothing about the repo state differed. The collapse happened anyway, and it landed on the floor named in this issue:

    23:51:00Z  last turn before the 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
    

    cache_read = 21,994. That is the ~21k shared-boilerplate floor this issue describes, reached without the git status trigger and without -p. Whatever the dynamic-system-prompt instability is, it is not confined to headless spawn-per-turn integrations in dirty repos — a normal --resume reaches it too.

    The prompt did not expire, it changed: 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. The rebuilt prefix was also 74,544 tokens smaller than the one sent before the pause — 816,300 against 890,844. An expired cache re-writes identical content. This re-wrote different content. No compaction record anywhere in the transcript.

    What it cost, in the units a subscriber actually feels. The account's weekly_scoped meter (the Fable bar behind /usage) was sampled every 15 minutes across the whole window. It read 17% before the resume and 20% after. Calibrating that meter against 16 integer percent-crossings in the same window puts cache-write tokens at ~0.087 output-token-equivalents (worst-case residual 0.24 points at that weight, versus 2.24 points for an output-only model):

    the resumed turn (3 requests):  out=4,031   cache_write=798,072
      output tokens alone   : 0.16 points
      output + cache writes : 2.87 points        <- 95% of the charge is cache-write
    

    ~3% of a weekly cap, consumed by one sentence of typed input, for content that had already been sent and was still sitting in a live cache. Roughly one point per 294,000 tokens re-cached. Which means the cost scales with how much good work is already in the session: the longer and more valuable the conversation, the more expensive it is to ask it one more question. Ending a nine-hour session and having a single afterthought is the most natural thing a user does, and it is the worst-priced action available to them.

    The part that makes this sting: the product already warns about expensive reopens — just not this one. When a session's context is large or stale, the CLI surfaces the offer to /clear and start fresh. That safeguard fires exactly when a user would already suspect the cost. It stays silent here, in the one case where the user has every reason to believe the resume is cheap: short pause, well inside the advertised 1-hour TTL, cache demonstrably live one turn earlier. The guardrail covers the intuitive case and misses the counter-intuitive one.

    Detection is already implemented; only the surfacing is missing. The 2.1.229 binary carries a cache-diagnosis path — [PROMPT CACHE BREAK], [PROMPT CACHE] cache deletion applied, cache read: , and a state comparator tracking systemHash, toolsHash, cacheControlHash, perBlockHashes, messageHashes and a buildDiffContent callback behind a cacheDiagnosis flag. The CLI can already tell which block moved and what it will cost. It just doesn't say so before spending the user's cap.

    Minimum viable fix, in ascending order of effort:

    1. Warn before sending. On resume, compare the rebuilt prefix against the session's last-sent prefix and print the damage: "resuming will re-cache ~794k tokens (~3% of your weekly cap) — press enter to continue, or start a fresh session." The comparison logic is in the binary already. This alone would have prevented the charge described above; the user would have opened a new window.
    2. Pin the prefix. When a live cached prefix exists for a session, reuse it for the remainder of that session's life rather than regenerating dynamic sections.
    3. Stabilise the dynamic blocks. The structural fix this issue and its predecessors are asking for.

    Prior art, for whoever triages this. The interactive-resume form of this defect was filed as #43657 (April 2026) and refiled as #67497 (documenting that #43657 was closed by the inactivity bot on 2026-06-11 as not_planned, not because it was fixed). #67497 was then itself closed as stale on 2026-07-21. #66005 is the same collapse via a different trigger (--resume dropping the session's --effort), still open. #40524 is the same token signature, closed.

    That is the same defect filed four times across five months, twice killed by inactivity sweep, never fixed. Adding this here rather than opening a fifth issue.

    Environment: Claude Code 2.1.229 (native, macOS 27.0), model claude-fable-5, 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 if useful.

  2. shangaslammi commented on Sep 6, 2026

    @shangaslammi

    Confirming on Linux (WSL2), Claude Code 2.1.263, Haiku 4.5, Max subscription (1-hour cache tier), from a headless spawn-per-turn host. Three additions that I did not see covered above.

    1. --resume --fork-session is affected identically, and the fork's system prompt is built at the first turn, not at process spawn. A fork process started with --input-format stream-json before a tree change, and given its first user turn after it, is cold. A fork started and turned right after that one is warm. So a host cannot "reserve" a warm fork by spawning early.

    2. Concurrent first turns of several forks race the cache write and all go cold. Two forks of the same parent turned at the same time after a commit both read the ~6.3k floor and both re-created ~5.5k; a third fork turned after they finished read the full prefix with zero creation. Sequencing the first one and then fanning out avoids it, but nothing in the docs suggests this is needed.

    Per-call usage of the first assistant message (read / creation), all forks from one bootstrap session in a minimal repo:

    case read / creation
    fork spawned before an untracked file appeared, first turn sent after 6 277 / 5 497
    next fork, turned right after that one 11 774 / 0
    two forks turned concurrently after a clean git commit 6 277 / 5 501 each
    fork turned after those two finished 11 778 / 0

    Note the clean commit case: nothing in git status --short changed, only the git log -n 5 lines, and that is enough.

    3. A long-lived --input-format stream-json process is unaffected, which is a better workaround than the non-repo cwd for hosts that can keep the process alive. Same repo, same three turns, untracked file added before turn 2 and a clean commit before turn 3:

    turn one process, turns over stdin (read / creation) one process per turn with --resume (read / creation)
    1 0 / 11 394 6 277 / 5 117
    2 (after untracked file) 11 600 / 151 6 277 / 5 483
    3 (after clean commit) 11 751 / 161 6 277 / 5 667

    The idle process makes no calls between turns, --json-schema applies per turn, and a stdin control_request interrupt leaves it usable and warm. The downside is that it is only available to hosts that own the process; interactive --resume and every fork still pay.

    Minimal repro (bash, Haiku, a few cents):

    d=$(mktemp -d); cd "$d"; git init -q; echo apple > a.txt; git add -A; git commit -qm init
    u() { python3 -c 'import sys,json
    for l in sys.stdin:
        j=json.loads(l)
        if j.get("type")=="assistant":
            u=j["message"]["usage"]; print("read=%d creation=%d"%(u["cache_read_input_tokens"],u["cache_creation_input_tokens"])); break'; }
    A=$(uuidgen); B=$(uuidgen)
    claude -p "Reply with the single word hi." --output-format stream-json --verbose --model haiku --session-id $A | u   # cold
    claude -p "Reply with the single word hi." --output-format stream-json --verbose --model haiku --resume $A     | u   # warm
    git commit -q --allow-empty -m churn
    claude -p "Reply with the single word hi." --output-format stream-json --verbose --model haiku --resume $A     | u   # cold again: read drops to the floor
    claude -p "Reply with the single word hi." --output-format stream-json --verbose --model haiku --resume $A --fork-session --session-id $B | u   # warm only because the previous call re-cached the new prefix

    I could not find a switch: in the 2.1.263 binary the block is skipped only when the cwd is not a git repository, and CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS governs something else. Freezing the git section to session-start values for -p --resume/--fork-session, or placing it after the message history, would fix both the resume and the fork cases.

  3. github-actions commented on Nov 12, 2026

    @github-actions

    Closing for now — inactive for too long. Please open a new issue if this is still relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:corearea:costbugSomething isn't workinghas reproHas detailed reproduction stepsplatform:windowsIssue specifically occurs on WindowsstaleIssue is inactive

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions