Keep Claude Code prompt cache warm to cut API costs
claude-thermos keeps Claude Code's prompt cache warm while a subagent runs long, avoiding the expensive full re-encode when the main agent resumes.
Why it matters
Prevent expensive prompt cache rebuilds during Claude Code sessions by automatically warming the cache when the main agent waits on subagents, eliminating the 20% of your bill spent re-encoding conversation history after 5-minute cache expirations.
Outcomes
What it gets done
Monitor Claude Code API traffic to detect when main agent cache is at risk of expiring
Send minimal warming requests to refresh the prompt cache before 5-minute TTL expires
Track token usage and calculate net savings from avoided cache rewrites
Log detailed session events and generate cost-benefit summaries for each coding session
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/izeigerman-claude-thermos | bash Overview
Claude Thermos
claude-thermos is a transparent local proxy wrapper for Claude Code that keeps the main agent's prompt cache warm while a subagent runs longer than the 5-minute cache TTL, issuing cheap zero-token warm requests directly to Anthropic so the main agent resumes with a cache read instead of an expensive full rewrite - measured to save about 22% of the bill on long sessions. Use it as a drop-in replacement for the claude CLI on any session with long-running subagents; it only activates when the main agent is idle with a subagent active, so it's unnecessary for sessions without long subagent calls.
What it does
claude-thermos stops Claude Code from silently rebuilding its prompt cache - and paying full write-rate price for it - when the main agent waits on a subagent for more than 5 minutes. Claude Code's prompt cache uses a 5-minute TTL: as long as requests on the same prefix keep arriving within that window, the whole conversation history is served from cache at 0.1x the input price instead of being re-sent at full price. The dominant trigger for that window expiring isn't the user thinking - it's the main agent blocked on a subagent that runs longer than 5 minutes. A subagent has its own system prompt and tool set, so its requests sit on a different cache prefix and never refresh the main agent's; while it works, the main agent's cached history ages untouched, and once 5 minutes pass it's gone. When the subagent returns, the main agent resumes with the same append-only history, finds no cache, and is forced into a full re-encode at the 1.25x write rate - by then the history is often 200K-500K tokens, making the rebuild expensive. Measured across roughly 185 local sessions, these rebuilds accounted for about 22% of the total bill.
claude-thermos fixes this by launching Claude Code behind a small local reverse proxy (pointing ANTHROPIC_BASE_URL at a loopback port; all real traffic still reaches the actual Anthropic API unchanged). The proxy watches /v1/messages traffic and groups it into sessions and "lineages" - one lineage per cache prefix, keyed by model, tool set, and system text; the first tool-bearing lineage is the main agent, the rest are subagents. When the main lineage goes idle while a subagent is actively running, the main prefix is flagged as at risk of expiring. On an interval kept under the 5-minute TTL, claude-thermos replays the main agent's last real request as a "warm request" - the identical cacheable prefix, but with max_tokens: 1 and no streaming, so the single generated token is thrown away and the only real effect is the prefill reading and refreshing the cached prefix. Warm requests go directly to the Anthropic API, never through the proxy, so they can't interfere with real traffic. When the subagent finishes and the main agent resumes, its cache is still warm, so it pays a cheap 0.1x read instead of a 1.25x rewrite on a much larger prefix - each warm costs a read, each rewrite it prevents would have cost far more, so the trade is heavily favorable.
Every session writes structured logs to ~/.claude-thermos/logs/<session_id>/: an append-only events.jsonl recording each request's token usage and every warming decision (warm_fired, warm_result, cap_reached, resume_detected), and a summary.json rollup with warms_fired, cache_read_total, successful-resume episodes, rewrite_avoided_tokens, warm_cost, rewrite_avoided_cost, and net_savings - all in base-input-token units, convertible to dollars by multiplying by your model's per-token input price.
When to use - and when NOT to
Use claude-thermos on any Claude Code session that spawns subagents running longer than a few minutes - long research tasks, multi-step subagent delegation, or workflows where the main agent regularly waits on background work - since that's exactly the pattern that silently expires the main agent's prompt cache and forces an expensive rewrite. It's a drop-in replacement for the claude invocation itself (uvx claude-thermos in place of claude), so there's no workflow change beyond how you launch the CLI.
It's unnecessary for sessions with no long-running subagents, since there's nothing for warming to protect - the tool only activates when the main agent goes idle with a subagent actively running. It requires Python 3.11+ and the claude CLI on PATH, and it can be disabled per-run with CLAUDE_WARMER_DISABLE=1 without changing the invocation.
Inputs and outputs
Run Claude Code through the wrapper instead of directly:
uvx claude-thermos # instead of: claude
uvx claude-thermos -p "fix the bug" # any claude args pass straight through
Input is any normal Claude Code invocation and its arguments, passed straight through unchanged. Optional tuning flags control the idle threshold before warming starts (--idle, default 270s), the interval between warm cycles (--interval, default 270s), the max warms per idle episode (--max-cycles, default 4, or auto), and how long a subagent counts as still active (--subagent-window, default 540s). Output is Claude Code running exactly as normal, plus per-session structured logs (events.jsonl, summary.json) reporting warms fired, cache reads, avoided rewrite tokens, and net savings.
Integrations
claude-thermos integrates transparently with the Anthropic API by intercepting Claude Code's traffic through a local reverse proxy on ANTHROPIC_BASE_URL, while warm requests bypass the proxy and go directly to Anthropic so they never disturb real conversation traffic. It requires no changes to Claude Code itself or to how agents and subagents are invoked - it observes standard /v1/messages traffic to build its session and lineage model.
Who it's for
Claude Code users running sessions with long-lived subagents - research, multi-step delegation, background work - who want to stop paying the roughly 20%-of-bill cost of prompt-cache rebuilds triggered by subagent wait times, without changing how they invoke Claude Code.
Source README
claude-thermos
Stop paying to rebuild your Claude Code cache. When your main agent waits on a subagent for more than 5 minutes, its prompt cache silently expires, and the next turn re-encodes your entire conversation at the write rate instead of reading it back cheap. On long sessions with many subagents that's roughly 20% of your bill. claude-thermos keeps the cache warm so you never pay that tax.
Use
Run Claude Code exactly as you normally would, but through claude-thermos with uvx:
uvx claude-thermos # instead of: claude
uvx claude-thermos -p "fix the bug" # any claude args pass straight through
Requires Python 3.11+ and the claude CLI on your PATH.
That's it. Warming runs automatically in the background. To disable it for a run without changing the command, set CLAUDE_THERMOS_DISABLE=1.
Tuning (all optional):
| Flag | Default | Meaning |
|---|---|---|
--idle |
270 |
Seconds the main agent must be idle before warming kicks in |
--interval |
270 |
Seconds between warming cycles |
--max-cycles |
4 |
Max warms per idle episode (auto for unlimited) |
--subagent-window |
540 |
Seconds a subagent counts as "still active" |
Choosing which claude to run
By default claude-thermos launches the claude found on your PATH. Point
it at a different binary with --bin, or the CLAUDE_THERMOS_BIN environment
variable. A bare name is looked up on PATH (so --bin claude-nightly
works); a full path is used as-is. This is handy for a vendored build or a
wrapper that exports a different CLAUDE_CONFIG_DIR per account.
claude-thermos --bin /path/to/bin/claude -p "fix the bug"
# or
export CLAUDE_THERMOS_BIN=/path/to/bin/claude
claude-thermos -p "fix the bug"
The flag must come before any passthrough claude args.
Daemon mode (shared proxy for the IDE and multiple terminals)
The default command warms only the claude process it launches. Clients that
launch claude themselves - the VSCode/Claude Code extension, which spawns
its own bundled binary - never go through it, and neither do other terminals.
claude-thermos serve runs the warming proxy as a standalone daemon on a
fixed loopback port. Point any client at it and they all share one warmer:
claude-thermos serve --port 8787 # run the daemon (Ctrl-C / SIGTERM to stop)
# then, for any client:
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
claude -p "fix the bug" # terminal — warmed by the daemon
For the VSCode extension, make sure its process inherits that environment
variable (on macOS, launchctl setenv ANTHROPIC_BASE_URL http://127.0.0.1:8787
before launching the app; or export it in the shell you start the editor from).
The extension honors ANTHROPIC_BASE_URL, so its traffic then flows through the
daemon and its main agent stays warm while subagents run.
The daemon observes traffic exactly like the launcher and already tracks many
sessions at once, so a single daemon serves every client on the machine. It
evicts sessions idle longer than --session-ttl (default 3600s) so it can run
indefinitely.
Tuning: serve accepts the same --idle/--interval/--max-cycles/--subagent-window
flags as the default command, plus:
| Flag | Default | Meaning |
|---|---|---|
--port |
8787 |
Loopback port the daemon listens on |
--upstream |
https://api.anthropic.com |
Real API the proxy reverse-proxies to |
--session-ttl |
3600 |
Seconds a session may sit idle before eviction |
Caveat:
--upstreammust be the real API, never the daemon's own loopback
address - otherwise the proxy would forward to itself.serverejects a
loopback upstream, so if you exportANTHROPIC_BASE_URLglobally, still start
the daemon with an explicit--upstream https://api.anthropic.com.
Why your cache keeps expiring
Claude Code's prompt cache uses a 5-minute TTL. Every turn, your whole conversation history is served from cache at 0.1x the input price instead of being re-sent at full price, as long as the cache stays alive.
The cache expires if more than 5 minutes pass between requests on the same prefix. The dominant trigger for that gap is not you thinking. It's the main agent blocked on a subagent that runs longer than 5 minutes. A subagent has a different system prompt and tool set, so its requests have a different cache prefix and never refresh the main agent's. While the subagent works, the main agent's cached history ages untouched; past 5 minutes it's gone. When the subagent returns, the main agent resumes with a byte-identical, append-only history, and finds its cache missing, forcing a full re-encode at the 1.25x write rate.
By then the history is large, so the re-encode is expensive: individual collapses re-write 200K to 500K tokens. Measured across roughly 185 local sessions, these rebuilds accounted for about 22% of the total bill, money spent re-encoding content that was already cached moments earlier.
How it works
claude-thermos launches Claude Code behind a small local reverse proxy (it points ANTHROPIC_BASE_URL at a loopback port; all traffic still goes to the real Anthropic API).
- Observe. The proxy watches
/v1/messagestraffic and groups it into sessions and lineages, a lineage being one cache prefix, keyed by model + tool set + system text. The first tool-bearing lineage is the main agent; the rest are subagents. - Detect the danger window. When the main lineage goes idle and a subagent is actively running, the main prefix is at risk of expiring.
- Warm. On an interval under the 5-minute TTL, it replays the main agent's last real request as a warm request: identical cacheable prefix, but
max_tokens: 1and no streaming. The single token is thrown away; the point is the prefill, which reads and refreshes the full cached prefix. Warm requests go directly to the API, never through the proxy, so they can't disturb real traffic. - Result. When the subagent finishes, the main agent's cache is still warm. It pays a cheap read instead of a full rewrite.
Each warm costs a cache read (0.1x); each rewrite it prevents would have cost a write (1.25x) on a much larger prefix, so the trade is heavily in your favor.
Event logs & savings
Every session writes to:
~/.claude-thermos/logs/<session_id>/
├── events.jsonl # append-only structured event stream
└── summary.json # rollup totals, written when the session ends
events.jsonl records each request/response's token usage plus every warming decision (warm_fired, warm_result, cap_reached, resume_detected, and so on). summary.json is the rollup you'll usually read:
| Field | Meaning |
|---|---|
warms_fired |
Warm requests sent |
cache_read_total |
Tokens read back by those warms |
episodes |
Idle-with-subagent episodes that ended in a successful resume (a rewrite actually avoided) |
rewrite_avoided_tokens |
Tokens that would have been re-written, summed across episodes |
warm_cost |
What warming cost you: 0.1 × cache_read_total |
rewrite_avoided_cost |
What it saved: 1.25 × rewrite_avoided_tokens |
net_savings |
rewrite_avoided_cost − warm_cost |
All three cost figures are in base-input-token units (token counts already weighted by their cache multiplier). To turn net_savings into dollars, multiply it by your model's price per input token:
dollars saved ≈ net_savings × (input token price)
For example, at an input price of $3 / 1M tokens, a net_savings of 1_200_000 is about 1_200_000 × $3 / 1_000_000 = $3.60 saved that session.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.