English · 简体中文
Keep Claude Code's prompt cache warm across idle pauses — without freezing your terminal.
A Stop hook stamps "the session was active just now"; a background
Monitor watches that timestamp and, only once the session has been idle
long enough (default 50 min), emits one line. That line is delivered to
Claude as a notification, which starts a fresh turn → a cache read →
the prompt-cache TTL is refreshed. One cheap read instead of an expensive
full cache rewrite.
Built for a 1 hour cache TTL. On a 5-minute TTL you would set
CCKA_IDLE_SECONDS=240.
10 warm pings (cache reads) vs. one cold start (a 1-hour cache rewrite), USD:
| Model | 100K ctx | 1M ctx | Savings |
|---|---|---|---|
| Opus 5 / 4.8 / 4.7 / 4.6 | $0.50 → $1.00 | $5.00 → $10.00 | 2× |
| Sonnet 5 | $0.20 → $0.40 | $2.00 → $4.00 | 2× |
| Sonnet 4.6 / 4.5 | $0.30 → $0.60 | $3.00 → $6.00 | 2× |
| Haiku 4.5 | $0.10 → $0.20 | $1.00 → $2.00 | 2× |
| Fable 5.1 | $0.25 → $2.00 | $2.50 → $20.00 | 8× |
warm ×10 → cold ×1. 10 pings ≈ 8 hours of warmth; pinging stops after
12 h idle, which keeps it under the 20-ping break-even for a 1-hour rewrite.
Full table (20K–1M, all models, break-even) and the generator:
docs/COST.md.
✅ For: Claude Code on official Anthropic — both a Claude Pro/Max subscription and a Console API key. On a subscription the main conversation gets a 1-hour prompt-cache TTL (within your plan's included usage); leave a session idle longer than that and your next message re-processes the whole prefix — slower, and far more usage than a cache read. This keeps the prefix warm so you come back to a cheap cache read. The defaults (50 min) are tuned for that 1-hour TTL.
On an API key the default TTL is 5 minutes, so lower the numbers or set a 1-hour TTL yourself — see Timing rule.
🔌 Other APIs / gateways? The plugin never inspects your provider, so any endpoint works — DeepSeek, GLM, OpenRouter, a self-hosted proxy, any Anthropic-compatible gateway — as long as the Monitor tool starts normally. Whether it starts is decided entirely by Claude Code, not by this plugin: the Monitor tool is first-party only and is skipped when the telemetry-disable env vars below are set or on Bedrock / Vertex / Foundry. If your setup can start a monitor, the keepalive runs on it unchanged — just keep in mind that other services may have different cache semantics (a cache read may not refresh the TTL the same way), so the cost math below may not hold.
turn ends (Stop) ─────────────► stamp hook: last_stop = now (instant, non-blocking)
you submit (UserPromptSubmit) ─► stamp hook: last_stop = now
background Monitor loop (dies with the session):
every TICK (default 300s):
idle = now - last_stop
if idle < IDLE_SECONDS: do nothing
else: echo "<ping>" ─► Claude Code delivers it as a notification
└► new turn → cache READ → TTL refreshed
└► turn ends → Stop → last_stop = now → ↻
Because the timer is now - last_stop, it is a resettable idle timer:
every Stop restarts the 50-minute window. An actively used session is never
pinged; only a genuinely idle one is.
Per-session: each session starts its own monitor, and the heartbeat/log
are keyed by the Claude Code session id, so concurrent sessions keep
independent idle timers — activity in session A does not postpone the ping
for an idle session B. The id is handed over explicitly (monitors.json runs the
monitor with --session "${CLAUDE_SESSION_ID}"), because Claude Code substitutes
that token inside a configured command string but does not export it to the
process environment (anthropics/claude-code#47018).
If no id is visible the monitor falls back to the shared last_stop file (any
activity resets it, so it never spams).
The heartbeat is per session and persists on disk. When you resume a session after a long gap, the stored heartbeat is already older than the idle window, so the monitor resets it to "now" on startup instead of firing an immediate ping against a cold cache. It then behaves normally and pings only after a fresh idle window. (The cache is cold anyway after a few hours.)
Stale monitor.<session>.pid files are harmless: the SessionEnd cleanup
only kills a pid whose command line actually matches the monitor, so a
reused pid is left alone.
Per-session state would otherwise accumulate forever. At session start (at
most once per CCKA_HOUSEKEEP_INTERVAL), the monitor runs a housekeeping
pass that:
- deletes
monitor.<session>.pidfiles whose process is gone; - bundles
last_stop.<session>/cache-keepalive.<session>.logolder thanCCKA_RETENTION_DAYS(default 7) intoarchive/cache-keepalive-<date>.tar.gz, bucketed by the file's last-modified date, then removes the originals; - optionally prunes archives older than
CCKA_ARCHIVE_KEEP_DAYS.
The current session's files, config, and the housekeeping logs are never
touched. Archives are ordinary tarballs:
ls ~/.claude/cache-keepalive/archive/
tar -tzf ~/.claude/cache-keepalive/archive/cache-keepalive-2026-09-21.tar.gz| Stop hook that sleeps | Monitor (this repo) | |
|---|---|---|
| Blocks the UI while waiting | yes (press Esc) | no |
| Limited by the 8-consecutive-block cap | yes | no |
| Can wait ~50 min in one shot | no (hook timeout) | yes |
| Cost while idle | — | one local sleep, 0 tokens |
| When it pings | after every turn | only after real idle |
Anthropic prices a cache read at ~0.1× base input, a 5-minute cache write at ~1.25×, and a 1-hour cache write at ~2×. When an idle gap lets the cache expire, the next turn re-processes the whole prefix at 1.25–2× instead of reading it at 0.1×. On an API key that is a direct bill; on a Pro/Max subscription it is what drains your plan usage faster.
At a 20K-token cached prefix (Sonnet-class, per million tokens):
| multiplier | cost of one idle refresh | |
|---|---|---|
| keepalive ping (cache read) | 0.1× | ~$0.006 |
| forced rewrite (cache expired) | 1.25× | ~$0.075 |
That is ~20 keepalive pings for the price of one forced 1-hour rewrite (12 on a 5-minute TTL). Covering a full 1 h TTL costs a handful of cache reads, so a couple of long breaks a day already pays for it. If your pauses are always shorter than the TTL, this does nothing for you — leave it off.
Full per-model tables (Opus / Sonnet / Haiku / Fable × 20K–1M context) and the break-even math: docs/COST.md.
/plugin marketplace add demouo/claude-code-cache-keepalive
/plugin install cache-keepalive@claude-cache-tools
Non-interactively:
git clone https://github.com/demouo/claude-code-cache-keepalive.git
claude plugin marketplace add ./claude-code-cache-keepalive
claude plugin install cache-keepalive@claude-cache-tools
claude plugin listThe plugin ships a monitors/monitors.json with "when": "always", so the
idle monitor starts automatically with the session.
The Monitor reads env vars first, then ~/.claude/cache-keepalive/config
(KEY=VALUE lines), then defaults. (Plugin userConfig values are not
visible to monitors, so use the config file or environment.)
| Env var | Default | Meaning |
|---|---|---|
CCKA_IDLE_SECONDS |
3000 |
Idle time before pinging (50 min) |
CCKA_TICK_SECONDS |
300 |
Poll granularity (5 min) |
CCKA_MAX_IDLE_SECONDS |
43200 |
Stop pinging once idle exceeds this (12 h; 0 = never) |
CCKA_PING_TEXT |
Reply with "ok" and nothing else. |
The line delivered to Claude |
CCKA_STATE_DIR |
~/.claude/cache-keepalive |
State + log directory |
CCKA_LOG |
$CCKA_STATE_DIR/cache-keepalive.<session>.log |
Explicit log path override |
CCKA_ENABLED |
1 |
0 / false / no / off disables the monitor |
CCKA_RETENTION_DAYS |
7 |
Age before per-session state is archived |
CCKA_ARCHIVE_KEEP_DAYS |
0 |
Prune archives older than this (0 = keep forever) |
CCKA_ARCHIVE_DIR |
$CCKA_STATE_DIR/archive |
Where dated archives go |
CCKA_HOUSEKEEP_INTERVAL |
21600 |
Min seconds between housekeeping runs (0 = every start) |
worst-case ping time = IDLE_SECONDS + (TICK_SECONDS - 1)
Keep that below the cache TTL:
| TTL | where | CCKA_IDLE_SECONDS |
CCKA_TICK_SECONDS |
worst case |
|---|---|---|---|---|
| 1 h (3600 s) | Claude Pro/Max within plan, or CLAUDE_CODE_PROMPT_CACHE_TTL=1h |
3000 (50 min) | 300 (5 min) | 55 min ✅ |
| 5 min (300 s) | API key default | 240 | 15 | 254 s ✅ |
A 5-minute tick is fine for a 1 h TTL, but not for a 5-minute TTL — lower the tick if you lower the TTL.
Log (per session):
tail -f ~/.claude/cache-keepalive/cache-keepalive.<session-id>.log./test.shChecks the stamp hook (writes last_stop, records session id, non-blocking)
and the idle timer (no ping while re-stamped, ping after idle, fresh Stop
postpones the next ping, CCKA_ENABLED=0 is a no-op). No Claude Code
required.
The monitor auto-starts with the session, but it does not have to:
| Want | Do |
|---|---|
| Stop it right now | delete it in Claude Code's task list (x), or /cache-keepalive:off |
| Stop it for this session only (default) | /cache-keepalive:off (per-session marker; other and new sessions unaffected) |
| Stop it everywhere, for good | cache-keepalive-ctl.sh off-all (global marker) |
| Don't start it in this project | touch <project>/.claude/cache-keepalive-off |
| Don't start it anywhere | CCKA_ENABLED=0 in ~/.claude/cache-keepalive/config |
| Turn this session back on | cache-keepalive-ctl.sh on, then /reload-plugins |
| Turn everything back on | cache-keepalive-ctl.sh on-all, then /reload-plugins |
| See what's running | /cache-keepalive:status |
The plugin ships exactly two commands, off and status, and both set
disable-model-invocation: true, so Claude never loads them on its own: they
are manual controls, not something the model should decide to run. Everything
else lives in the same script, which edits the marker files directly. Those
markers (disabled, disabled.<session>, CCKA_ENABLED) are the single
source of truth the monitor reads — the commands are only a convenient way to
flip them.
/cache-keepalive:off is session-scoped: it stops only the monitor belonging
to the session you are in and writes
~/.claude/cache-keepalive/disabled.<session>. That session id then skips
auto-start, while every other open session and every future session keeps
working normally — useful when you just want the current conversation to stop
pinging. The ctl script's on clears it.
cache-keepalive-ctl.sh off-all is the blanket switch: it stops every monitor
and writes ~/.claude/cache-keepalive/disabled. The monitor checks that global
marker on every start, so a later /reload-plugins or a brand new session
will not silently bring it back. on-all clears the global marker and every
per-session marker.
Deleting the task by hand is fine too — state stays consistent (stale pid files are cleaned up, and a reused pid is never killed by mistake).
The full control set is a plain script:
bash plugins/cache-keepalive/scripts/cache-keepalive-ctl.sh status|on|off|on-all|off-allclaude plugin uninstall cache-keepalive # hooks, monitor and skills go with it
rm -rf ~/.claude/cache-keepalive # optional: also drop state + logs- Monitor availability. The Monitor tool needs a recent Claude Code and
is not available when
DISABLE_TELEMETRYorCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICis set, nor on Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry. Plugin monitors run only in interactive CLI sessions. The plugin itself does not check the provider: if your setup can start a monitor — any API, any gateway — the keepalive runs there too. - Usage. Works on both a Pro/Max subscription and API billing. On a
subscription the point is to avoid a full prefix re-processing (which drains
plan usage) after an idle gap longer than the 1-hour TTL, not to cut a
per-token bill. Pinging stops after
CCKA_MAX_IDLE_SECONDS(default 12 h) so an abandoned session does not keep pinging. - Multi-session. Each session runs its own monitor and heartbeat
(
last_stop.<session_id>), so timers are independent. The only shared case is a monitor that cannot seeCLAUDE_SESSION_ID, which falls back to the globallast_stop. - The ping is a real turn. It appears in the transcript and costs one
cache read + a short reply. Keep
CCKA_PING_TEXTbland; the default already asks only for a one-wordok, so the added turn stays as small and as unobtrusive as possible. - Experimental. Plugin monitors are an experimental component and run unsandboxed at hook trust level.
- Provider. The keepalive runs on any provider whose Claude Code can start a Monitor. That said, "a cache read refreshes the TTL" is Anthropic's documented behaviour; third-party Anthropic-compatible gateways may differ, so the savings are not guaranteed there.
- Exit confirmation. Claude Code shows a "Background work is running …
Exit anyway?" prompt whenever a session-scoped monitor is active
(anthropics/claude-code#58852, closed as not planned — there is no
silentExitopt-out). This plugin ships aSessionEndhook that stops the monitor during teardown; whether that removes the prompt depends on the exit ordering, so you may still see it. The default selection is Exit anyway, so a plain Enter dismisses it.
.
├── .claude-plugin/marketplace.json # single-plugin marketplace
├── plugins/cache-keepalive/
│ ├── .claude-plugin/plugin.json # plugin manifest
│ ├── hooks/hooks.json # Stop + UserPromptSubmit -> stamp, SessionEnd -> cleanup
│ ├── monitors/monitors.json # auto-start idle monitor
│ ├── skills/{off,status}/SKILL.md # /cache-keepalive:* (manual controls)
│ └── scripts/
│ ├── cache-keepalive-stamp.sh # record last_stop
│ ├── cache-keepalive-monitor.sh # idle timer -> ping
│ ├── cache-keepalive-cleanup.sh # SessionEnd: stop the monitor
│ ├── cache-keepalive-housekeep.sh # archive stale state
│ └── cache-keepalive-ctl.sh # status | on | off | on-all | off-all
├── test.sh
└── README.md
- yujiachen-y/claude-code-cache-keepalive —
same goal, built as a
Stophook that sleeps before pinging. It runs on any provider, but it blocks the terminal while waiting and is capped at 8 consecutive blocks, so it can only cover ~30 min at a time. This repo uses the Monitor tool instead: no UI block, no block cap, and a real 50-min idle timer. Trade-off: it requires the Monitor tool — and where that tool can start, any provider works. - Aider
--cache-keepalive-pings, Cache-Refresh-SillyTavern, and cline/cline#414 — the same "a cache read refreshes the TTL" trick, in other tools.
- LINUX DO — an open, friendly Chinese-language tech community.
MIT — see LICENSE.