A Claude Code plugin that warns you before a prompt-cache miss re-writes your entire context — and lets you decide whether to pay for it.
You're deep in a session, get pulled away for an hour, and send another message. The cache has expired, so the whole accumulated context is re-processed as a cache write instead of a cheap cache read. A cold resume on a 140k-token context costs roughly 20× what a warm turn would have.
This isn't hypothetical. Measured across 12,407 assistant turns of real session history (2026-06-22 to 2026-08-03):
| Idle gaps longer than 1 hour | 56 |
| …that produced a cache write above 20k tokens | 54 (96%) |
| Total re-written on those events | 11.2M tokens |
| Largest single re-write | 695,109 tokens |
| Median cache write on a warm turn | 1,024 tokens |
| Share of all cache-write spend | 23% |
Fifty-four keystrokes accounted for close to a quarter of all cache-write cost. The signal is nearly clean: idle time predicts the expensive case with 96% precision, against a warm-turn population whose median write is three orders of magnitude smaller.
Those numbers are a snapshot of a corpus that grows every time you use Claude
Code. Run measure.py to regenerate the table against your own
history — no arguments, read-only, standard library only.
Before a prompt is sent, the plugin checks how long the session has been idle against the cache TTL the API actually applied. If the cache has probably expired and the context is large enough to matter, it blocks the prompt once:
⚠ Cache likely cold: idle 3h12m (TTL 1h).
~142k tokens will be re-written at 2× input rate (≈20× a warm turn).
Press ↑ and resend to proceed, or /clear to start fresh.
Press ↑ and resend and it goes straight through — this is a decision point, not a nag. Nothing is lost when the prompt is blocked: Claude Code preserves it in history and echoes it back in the block display.
claude plugin marketplace add Moishe/claude-cache-miss-detector
claude plugin install cache-miss-detector@cache-miss-detectorTo work on it locally, point the marketplace at a checkout instead:
claude plugin marketplace add /path/to/claude-cache-miss-detector. Note that
installing copies the files into ~/.claude/plugins/cache/, so edits to your
checkout don't take effect until you reinstall — for iterating on the hook,
wire it directly in settings.json instead.
Requires Python 3.8 or newer, standard library only. No jq, no npx, no
other dependencies, no install step.
All optional environment variables:
| Variable | Default | Meaning |
|---|---|---|
CACHE_GUARD_MIN_TOKENS |
20000 |
Don't warn below this estimated context size |
CACHE_GUARD_GRACE |
30 |
Seconds subtracted from the TTL, so the warning fires early |
CACHE_GUARD_TTL |
observed | Force a TTL in seconds instead of reading it from the transcript |
CACHE_GUARD_DISABLE |
unset | Any non-empty value turns the plugin off |
Two constants live in the source rather than the environment: the confirmation window is 120 seconds, and the transcript tail read is 1MB.
If the warnings feel too frequent, CACHE_GUARD_MIN_TOKENS is the knob —
raise it. Note that a fresh Claude Code session can already carry 50–60k
tokens from the system prompt, tool definitions, CLAUDE.md, and enabled
plugins, so the 20k default rarely suppresses anything in practice. That is
usually correct: even a "small" cold resume at 60k is real money.
Everything comes from the session transcript. There is no state file beyond a single confirmation marker in the temp directory, keyed by session id.
- Idle time is the age of the newest
assistantentry. Every tool round-trip produces one, so this measures time since the last API activity — not time since your last message, which is the wrong clock. - Context size is
input_tokens + cache_read_input_tokens + cache_creation_input_tokensfrom that entry's usage block. Median error against the next turn's actual write is 14%, p90 39%. - The TTL is observed, not inferred, from
usage.cache_creation.ephemeral_1h_input_tokensversusephemeral_5m_input_tokens— whichever bucket the API actually billed.
That last point does more work than it looks like it does. The obvious
implementation reads ENABLE_PROMPT_CACHING_1H and assumes 5 minutes when
it's unset. That is wrong for anyone on a Claude subscription, which gets the
1-hour TTL automatically without the variable. Guessing 5 minutes when the
real TTL is an hour means firing on every gap in the 5m–1h band — 354 of them
in this corpus, of which only 39 crossed the 20k floor. That's 11%
precision: about 315 spurious blocks to catch 39 more events. Reading the
bucket the API actually used is both simpler and correct.
The hook command in hooks/hooks.json ends with || true. That is not
sloppiness — it is the whole reason a broken guard can't lock you out of a
session.
If cache_guard.py is missing or unparseable, python3 exits non-zero
before any of the plugin's code runs, and Claude Code treats a non-zero exit
from a UserPromptSubmit hook as "block this prompt." Every subsequent prompt
blocks too. The script's own careful sys.exit(0) discipline can't help,
because the script never loads. || true makes the shell exit 0 regardless.
stderr is deliberately not suppressed, so a broken guard is noisy rather than silently dead.
This failure mode was found by live testing, not by the test suite — no test that runs inside a Python process can catch a failure that happens before the interpreter loads the file.
- The estimate excludes the new prompt and anything appended after the last assistant turn. That's the source of the measured 14% median error.
- Subagents always run on a 5-minute TTL even when the main conversation is on an hour. Out of scope — the main-conversation gate is where the spend is, and subagent turns are written to separate transcript files, so they don't contaminate the idle clock.
- If the last 1MB of transcript contains no assistant entry — a pathological single enormous tool output — the guard stays silent rather than widening the read.
- Any internal error fails open. A guard that breaks must never be able to block your input.
python3 test_cache_guard.py30 tests, no framework, no fixtures. The suite has been non-hermetic before, so if you're changing anything that touches the confirmation marker, run it two or three times rather than trusting a single green.
Design rationale and the full measurement methodology are in
DESIGN.md; the original problem statement is in
cache-miss-warning-spec.md;
PLAN.md is the implementation plan, kept as a historical record
and superseded where it disagrees with the code.