Skip to content

Repository files navigation

cache-miss-detector

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.

The problem

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.

What it does

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.

Install

claude plugin marketplace add Moishe/claude-cache-miss-detector
claude plugin install cache-miss-detector@cache-miss-detector

To 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.

Configuration

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.

How it decides

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 assistant entry. 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_tokens from 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_tokens versus ephemeral_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.

Don't remove the || true

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.

Limitations

  • 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.

Development

python3 test_cache_guard.py

30 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.

About

Checks for possible cache misses on Claude sessions and warns.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages