Back to Discover

claude-code-cache-keepalive

prompt

yujiachen-y

Keep Claude Code's 5-minute prompt cache warm across idle pauses. A single-plugin marketplace that injects cheap keepalive turns via a Stop hook. Saves real money on idle API-billed sessions.

View on GitHub
15 starsMITSynced Aug 2, 2026

Install to Claude Code

/plugin marketplace add yujiachen-y/claude-code-cache-keepalive

README

claude-code-cache-keepalive

⚠️ EXPERIMENTAL — READ THIS BEFORE INSTALLING

This project is an experiment. It works by exploiting a currently-documented (but not contractually guaranteed) property of Anthropic's prompt cache: that cache reads refresh the 5-minute TTL. The economics assume you understand how it works. Used wrongly, this plugin will cost you more, not less.

Before you install, you need to understand at minimum:

  • It is only useful on API-key / token-metered billing. On Pro/Max subscription plans, your quota is gated by request count, not token cost — every keepalive turn eats your 5-hour quota. On subscriptions, this plugin makes things worse. Disable it.
  • Every keepalive turn is a real turn. It reads the cached prefix and produces a short reply. If your keepalive_message derails Claude into running tools or generating long output, the "cheap ping" stops being cheap.
  • The whole trick depends on one invariant (cache reads refresh the TTL). Anthropic can change that without notice — the same way they silently changed the default TTL from 1h to 5m in March 2026 (claude-code#46829). If they do, this plugin will quietly stop saving money and may start costing money.
  • You must watch for community updates. Follow the linked issues and the CHANGELOG of this repo. When the community reports a behavior change on Anthropic's side, re-tune your interval_seconds / max_loops_per_turn or disable the plugin entirely. A plugin you installed six months ago and forgot about is the exact scenario where this kind of trick silently turns against you.

Read the Theoretical basis, The economics, and Caveats and warnings sections in full before enabling this. If any of those sections don't make sense to you yet, that's the signal that you are not yet ready to install this plugin. By installing, you accept that you have read and understood the mechanism and are responsible for monitoring its ongoing economics yourself.


A Claude Code plugin that keeps the prompt cache warm across idle thinking pauses. It uses the Stop hook's decision: "block" mechanism to inject a tiny keepalive turn just before the 5-minute cache TTL expires. Cache reads cost ~12.5x less than cache writes, so this saves real money on long sessions with idle gaps.

Why this exists. In March 2026 Anthropic silently lowered Claude Code's default prompt-cache TTL from 1 hour to 5 minutes. Users with normal 5+ minute thinking pauses started getting billed for full cache rewrites on every turn. See anthropics/claude-code#46829 for the original issue and the ~$2.5k of excess billing one user traced to the change. This plugin is a workaround.


Install

This repo ships as both a Claude Code plugin and a single-plugin marketplace. Inside Claude Code:

/plugin marketplace add yujiachen-y/claude-code-cache-keepalive
/plugin install cache-keepalive@claude-cache-tools

That's it. No daemon, no tmux, no Python, no curl, no API key.

To verify the plugin is active:

/plugin

and look for cache-keepalive in the enabled list. You can also tail the hook log:

tail -f ~/.claude/cache-keepalive/cache-keepalive.log

How it works

When Claude finishes a turn, Claude Code fires the Stop hook. This plugin's Stop hook script sleeps for interval_seconds (default 240s, safely under the 300s TTL) and then writes this JSON to stdout:

{"decision":"block","reason":"got it, i need some time to think about the next move"}

Claude Code treats that as "the user sent a new instruction" and asks Claude to respond. The response reads the cached prompt prefix — and cache reads refresh the 5-minute TTL. Claude replies with one short line, Stop fires again, the loop continues.

                        ┌─────────────────────────────┐
   Claude finishes ───▶ │ Stop hook runs              │
   a turn               │   counter < max_loops ?     │
                        │     yes → sleep interval    │
                        │           emit JSON:        │
                        │           {"decision":      │
                        │            "block",         │
                        │            "reason": MSG }  │
                        │     no  → exit 0 (let Stop  │
                        │           proceed normally) │
                        └──────────────┬──────────────┘
                                       │ block
                                       ▼
                        ┌─────────────────────────────┐
                        │ Claude Code receives block, │
                        │ injects MSG as new turn,    │
                        │ Claude generates a reply.   │
                        │ The reply READS the cached  │
                        │ prefix → TTL is reset.      │
                        └──────────────┬──────────────┘
                                       │ Stop fires again
                                       ▼
                                  loop ↻

When you actually want to type, hit Esc to interrupt the sleeping hook. The UserPromptSubmit hook resets the loop counter for the next turn. The SessionEnd hook cleans up on exit.

The keepalive turn is a real turn: it shows up in your transcript and it hits the exact same cache_control block Claude Code uses for the real prompt prefix. No reverse engineering, no API hijacking, no Claude Code internals dependency.


Configuration

The plugin exposes three user-configurable values. When you install it, Claude Code will prompt you for each one. Leave a value blank to use the default.

KeyDefaultWhat it does
interval_seconds240Seconds the Stop hook sleeps before pinging. Claude Code's TTL is 300s; 240 leaves a 60s safety margin.
keepalive_messagegot it, i need some time to think about the next moveText injected as the user-side keepalive turn. Keep it bland so Claude replies cheaply.
max_loops_per_turn7Max consecutive keepalive cycles per idle turn. At 240 × 7 ≈ 28 min, this caps coverage and prevents a runaway loop if you walk away.

You can change these values any time via /plugin → select cache-keepalive → reconfigure. Or override them at shell level with CCKA_INTERVAL, CCKA_MESSAGE, CCKA_MAX_LOOPS — the hook scripts check plugin config first, then these env vars, then the defaults.


Theoretical basis

This plugin rests on one well-documented but easily forgotten property of Anthropic's prompt caching:

A successful cache read refreshes the cache's TTL.

If your cached prefix would have expired in 30 seconds but you send any request that reads it, the 5-minute clock resets from scratch. That means a cheap "do nothing" request right before expiry keeps the cache alive for another 5 minutes — and you only pay for a cache read, not a cache write.

This is the same trick Aider's --cache-keepalive-pings, the SillyTavern cache-refresh extension, and ClaudeMind's built-in pinger all use. It was discussed for Cline in cline/cline#414, which is the clearest write-up of the math. From that thread (cspotcode):

"If first message populates the cache at 1.25x cost, ping it 6 times at .1x cost, then send your second chat message...you still come out ahead: 1.25 + (6 + 1) × 0.1 = 1.95x cost for 2x requests."

That is: on a 6-minute idle gap, a naive session pays 2.5x base input cost (two full writes). A session with six 45-second-spaced pings pays only 1.95x — a ~22% savings on the idle gap, rising fast the longer the gap gets. ClaudeMind's implementation extends the effective TTL from 5 minutes to 60 minutes with ~12 pings and reports that the break-even is just two follow-up questions.


The economics (Claude Code specifics)

Anthropic's prompt caching pricing (Sonnet 4.6, per million tokens):

ItemMultiplier$/MTok
Base input1.0x$3.00
5-min cache write1.25x$3.75
1-hour cache write2.0x$6.00
Cache read0.1x$0.30

A cache read is ~12.5x cheaper than a 5-min cache write for the same number of tokens. Each keepalive turn is one read over the existing prefix plus a ~20-token reply. At a 20K-token cached prefix:

  • one keepalive turn: 20K × $0.30/M ≈ $0.006 + ~20 output tokens
  • one forced rewrite: 20K × $3.75/M ≈ $0.075

You can do roughly 12 keepalives before you've spent what a single rewrite would cost. The default max_loops_per_turn=7 stays well inside the break-even line.

For reference, the issue that prompted this plugin (anthropics/claude-code#46829) reports one user's traced excess billing at ~$949 on Sonnet-4.6 and ~$1,582 on Opus-4.6 after Anthropic silently flipped the default TTL from 1 hour to 5 minutes in March 2026 — with $1,198 of the Opus overcharge landing in a single month. Even at that user's scale, roughly two months of uncapped normal usage would have paid for a long time of running this plugin.


Caveats and warnings

  1. Use API-key billing, not Pro/Max subscriptions. Subscription plans are gated on a 5-hour request quota, not on token cost. Extra keepalive turns eat your quota. On Pro/Max this plugin makes things worse, not better — disable it.

  2. The keepalive turn is real. It shows up in your transcript, in your scrollback, and in your token history. Don't set keepalive_message to anything that derails Claude's reasoning or starts a long task.

  3. You have to know to hit Esc. While the Stop hook is sleeping, Claude Code looks "stuck" — no input prompt. Press Esc once; the sleep exits and the prompt comes back. Hook logs in ~/.claude/cache-keepalive/cache-keepalive.log confirm what's happening.

  4. Anthropic could change the rules. This whole trick depends on "cache reads refresh the 5-minute TTL," which is currently true and documented but is not a contractual API guarantee. The same Anthropic that silently lowered the TTL default could change refresh semantics too. Don't bet anything important on this.

  5. Hook runtime limits. Some Claude Code installs may warn about long-running hooks. The default 240s sleep is within typical timeouts, but if your config enforces stricter limits, lower interval_seconds accordingly.

  6. The Stop hook contract. This plugin relies on the documented {"decision":"block","reason":"..."} Stop-hook output. If a future Claude Code version changes that contract, the plugin will break loudly (keepalive disappears from the log) rather than silently leak budget.


File layout

claude-code-cache-keepalive/
├── .claude-plugin/
│   └── marketplace.json           # the marketplace catalog
├── plugins/
│   └── cache-keepalive/
│       ├── .claude-plugin/
│       │   └── plugin.json        # the plugin manifest + userConfig
│       ├── hooks/
│       │   └── hooks.json         # Stop / UserPromptSubmit / SessionEnd wiring
│       └── scripts/
│           ├── cache-keepalive-stop.sh     # the sleep-and-block loop
│           ├── cache-keepalive-reset.sh    # clear counter on real prompt
│           └── cache-keepalive-cleanup.sh  # clear counter on session end
├── LICENSE
└── README.md

Prior art

  • Aider's --cache-keepalive-pings — the same idea, implemented as a built-in feature inside Aider. This project ports the pattern to Claude Code via hooks and packages it as an installable plugin / marketplace.
  • Cache-Refresh-SillyTavern — same pattern for SillyTavern. Reports ~89% cost reduction on long conversations.
  • cline/cline discussion #414 — community discussion of the same math for the Cline editor.

License

MIT. See LICENSE.

Rendered live from yujiachen-y/claude-code-cache-keepalive's GitHub README — not stored, always reflects the source repo.

1 Plugin

NameDescriptionCategorySource
cache-keepaliveKeep Claude Code's 5-minute prompt cache warm across idle pauses. Uses a Stop hook to inject cheap keepalive turns before the TTL expires — cache reads cost ~12.5x less than cache writes, so this saves real money on API-billed sessions. API billing only; disable on Pro/Max.productivity./plugins/cache-keepalive

0 Comments

Login required
Log in to post a comment or update on this repo.

No comments yet — be the first to share an update.