by ARahim3
A tiny Claude Code skill that keeps your prompt cache warm during idle sessions, so your next message reads from cache instead of paying full price.
# Add to your Claude Code skills
git clone https://github.com/ARahim3/cachebeatGuides for using cli tools skills like cachebeat.
See how cachebeat compares with popular alternatives.
cachebeat is an open-source cli tools skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by ARahim3. A tiny Claude Code skill that keeps your prompt cache warm during idle sessions, so your next message reads from cache instead of paying full price. It has 51 GitHub stars.
cachebeat's catalog security scan is still queued. You can run an instant dependency and prompt-injection check now with the "Scan for vulnerabilities" button above.
Clone the repository with "git clone https://github.com/ARahim3/cachebeat" and add it to your Claude Code skills directory (see the Installation section above). cachebeat ships a SKILL.md manifest, so compatible agents can discover and load it automatically.
Yes. SkillsLLM lists many other CLI Tools skills you can browse and compare side by side. Open the CLI Tools category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh cachebeat against similar tools.
No comments yet. Be the first to share your thoughts!
Top skills in this category by stars
Unlocks once the catalog security scan passes (runs nightly).
⚠️ Third-Party Software Notice
This skill is third-party open-source software developed and hosted independently on GitHub. SkillsLLM is an informational directory and does not control or maintain the underlying repository.
Any security checks, ratings, or warnings displayed by SkillsLLM are automated and limited in scope. They do not constitute a security certification or guarantee that the software is safe, error-free, or free from malicious code, vulnerabilities, compromised dependencies, or prompt-injection risks.
Review the source code, permissions, dependencies, and configuration before installing or running any third-party skill. Use is at your own risk. To the maximum extent permitted by applicable law, SkillsLLM is not liable for losses arising from third-party software.
The deep catalog scan for this skill is still queued. Run an instant dependency check now instead.
Parse the arguments: /cachebeat [minutes] [max_hours] or /cachebeat stop.
minutes: heartbeat interval. Default 50. Clamp to 5–55 — above ~55 the 1-hour
cache TTL expires between beats and the heartbeat is pointless.max_hours: auto-stop deadline. Default 8. Clamp to 1–24. This is the abandoned-session
guard: every beat costs a small cached-price request, so a forgotten heartbeat must kill itself.stop: stop the running cachebeat monitor with TaskStop. Its description is
cachebeat keepalive; if its task ID is no longer in your context, list running background
tasks (TaskList or equivalent) to find it. Confirm in one short line. Do nothing else.The heartbeat is an inactivity timer, not a metronome: it fires only when the session has been quiet for N minutes. Any real turn — a user message, one of your replies, a background event you answered — resets the clock, because it already refreshed the cache.
Idle is measured from the timestamp of the last entry in this session's transcript, not from
the transcript file's mtime. This distinction is the whole point: a .jsonl file's mtime advances
on background rewrites (recap regeneration, atomic saves) that add no turn, so keying off mtime can
hide real idle minutes and make the beat fire late — after the TTL — so the beat itself becomes
the full-price uncached re-read it was meant to prevent. The monitor also pins this session's
transcript via $CLAUDE_CODE_SESSION_ID instead of "newest file in the folder", so other sessions
sharing the project folder can't hijack the clock.
Start ONE persistent Monitor exactly like this (substitute N = minutes, H = max_hours):
Monitor(
command: "SID=\"$CLAUDE_CODE_SESSION_ID\"; F=$(ls \"$HOME/.claude/projects\"/*/\"$SID\".jsonl 2>/dev/null | head -1); [ -z \"$F\" ] && { DIR=\"$HOME/.claude/projects/$(pwd | tr '/._' '---')\"; F=$(ls -t \"$DIR\"/*.jsonl 2>/dev/null | head -1); }; END=$(( $(date +%s) + H*3600 )); PREV=__init__; LAST=$(date +%s); while [ $(date +%s) -lt $END ]; do sleep 60; SIG=$(tail -n 1 \"$F\" 2>/dev/null | grep -oE '\"timestamp\":\"[^\"]+\"' | tail -1); [ -z \"$SIG\" ] && SIG=$(wc -c < \"$F\" 2>/dev/null); if [ \"$SIG\" != \"$PREV\" ]; then PREV=\"$SIG\"; LAST=$(date +%s); fi; IDLE=$(( $(date +%s) - LAST )); if [ $IDLE -ge $((N*60)) ]; then echo \"HEARTBEAT idle=${IDLE}s $(date +%T)\"; sleep 300; CR=$(tail -n 400 \"$F\" 2>/dev/null | grep -oE '\"cache_read_input_tokens\":[0-9]+' | tail -1 | grep -oE '[0-9]+'); CC=$(tail -n 400 \"$F\" 2>/dev/null | grep -oE '\"cache_creation_input_tokens\":[0-9]+' | tail -1 | grep -oE '[0-9]+'); [ -n \"$CC\" ] && [ -n \"$CR\" ] && [ \"$CC\" -gt \"$CR\" ] && echo \"CACHEBEAT_WARN beat re-read uncached: cache_creation=$CC > cache_read=$CR\"; fi; done; echo CACHEBEAT_EXPIRED",
description: "cachebeat keepalive",
persistent: true
)
How it works, piece by piece:
$CLAUDE_CODE_SESSION_ID names this exact conversation, so .../projects/*/<id>.jsonl
is its transcript no matter how many other sessions share the project folder. If that variable is
ever empty, it falls back to the newest transcript in the folder — correct at arm time because you
are mid-turn writing this very exchange.timestamp string, compared for change against the monitor's
own wall clock (date +%s). A genuine new turn changes it and resets the clock; a bare mtime bump
with no new line does not. No timestamp parsing, so it stays portable across macOS/Linux/WSL.sleep 300 after a beat gives your reply time to land so one idle period never double-fires.cache_creation vs cache_read; if the
beat came back mostly uncached it emits CACHEBEAT_WARN — the signal that the session's TTL is too
short for any heartbeat to help.Then confirm to the user in ONE line: interval, auto-stop time, and how to stop
(/cachebeat stop). Nothing more.
Reply with a few words only (e.g. "Okay, still here."). Do NOT summarize, do NOT run tools, do NOT re-arm anything — the monitor repeats by itself. If a real task of yours is also running, you may append its one-line status, nothing more. Minimal output is the entire point: every extra token you emit is added to the context that each future beat re-reads.
On CACHEBEAT_WARN …: the last beat fired on schedule but still re-read the context uncached, so
the keepalive is not paying off. In one line, tell the user their session likely has the short
(5-minute) cache TTL — or the interval is longer than their TTL — so no practical heartbeat helps,
and offer to stop it. Do not re-arm.
On CACHEBEAT_EXPIRED: tell the user the heartbeat reached its auto-stop deadline and is off,
one line. Re-arm only if they ask.
Stop paying full price to say "I'm back" to your own Claude Code session.
A tiny Claude Code skill that keeps your prompt cache warm while a session sits idle — so your next message reads the conversation from cache instead of re-billing it from scratch. Anthropic bills cache reads at 0.1× the input rate (published ratio, every model) — an uncached return costs ~10× more.
This is not just an API-billing thing. On Pro/Max subscriptions the same accounting drains your 5-hour window and weekly usage limit: come back to a long session cold, and one "how's it going?" can eat a visible chunk of your quota that a warm cache would barely have touched.
It was born in a real session: a multi-day model fine-tune babysat by Claude — training runs that took hours, checkpoints landing overnight, a human who occasionally sleeps. Every time the session went quiet for longer than the cache window, the next "how's it going?" re-billed the entire conversation history at full input price. On a long, tool-heavy session that's hundreds of thousands of tokens, re-billed every single time you stepped away too long.
One inactivity-triggered heartbeat later, it didn't.
Claude is stateless: your whole conversation is re-sent to the API on every turn. Anthropic's prompt caching makes this affordable — cached input costs ~10 % of the normal rate — but the cache expires after a TTL (up to 1 hour on long-TTL sessions). The failure mode:
Do that a few times a day on a long-running session and the "idle tax" quietly becomes the biggest line in your bill — or the reason you hit your usage limit by mid-week.
/cachebeat arms a single background monitor inside your session. It's an inactivity timer,
not a metronome: it fires only when the session has been truly silent for N minutes — any
message, reply, or answered background event already refreshed the cache and resets the clock.
On firing, Claude wakes, answers with a few words, and the cache TTL restarts.
Break-even math: going cold once costs as much as ten heartbeats. Come back to the session even once and the heartbeat has paid for itself many times over — and the bigger the session, the bigger the absolute savings (the 10× ratio is flat; the token count isn't).
It also kills itself after a deadline (default 8 h), so an abandoned session doesn't drip-bill forever.
User-level (all projects):
mkdir -p ~/.claude/skills/cachebeat
cp SKILL.md ~/.claude/skills/cachebeat/
Or project-level: copy SKILL.md into <your-project>/.claude/skills/cachebeat/.
That's the whole setup — copy one file. Then in any Claude Code session, type /cachebeat and
you're done; there's nothing to configure. (It just needs a Claude Code that runs background
tasks, which is the default.)
/cachebeat # fire after 50 min of inactivity, auto-stop after 8 h
/cachebeat 40 # custom idle threshold in minutes (clamped to 5–55)
/cachebeat 40 4 # custom threshold + auto-stop after 4 h
/cachebeat stop # stop it
You don't have to take it on faith — you can watch the cache stay warm.
The status line. Claude Code's token readout looks like tok:312.0k/0.0k — total / served
from cache. When you return to an idle session, the second number should be a large fraction of
the first (warm). If it reads …/0.0k, the whole context was re-read uncached — the cache had
died. With cachebeat armed on a long-TTL session, it should stay warm across your idle gaps.
The transcript (exact numbers). Every assistant reply records what it cost. Run this inside the
session (prefix a shell command with !):
F=$(ls ~/.claude/projects/*/"$CLAUDE_CODE_SESSION_ID".jsonl 2>/dev/null | head -1)
echo "cache_read: $(tail -n 400 "$F" | grep -oE '"cache_read_input_tokens":[0-9]+' | tail -1 | grep -oE '[0-9]+')"
echo "cache_creation: $(tail -n 400 "$F" | grep -oE '"cache_creation_input_tokens":[0-9]+' | tail -1 | grep -oE '[0-9]+')"
A warm turn shows a big cache_read and a tiny cache_creation. A cold turn is the
reverse — a large cache_creation means the context was rebuilt from scratch at full price.
The heartbeat itself. When a beat fires you'll see Claude answer with a few words ("Okay, still
here.") — proof the monitor is alive and resetting the TTL. If instead you see CACHEBEAT_WARN,
the beat fired on schedule but still came back uncached: your session's TTL is too short for any
heartbeat to bridge, so stop it.
Claude Code writes every exchange to a session transcript (~/.claude/projects/<slug>/<session-id>.jsonl).
The skill starts one persistent background monitor that, once a minute, checks when the last real
turn happened and how long ago that was:
# Pin THIS session's transcript by its id (not "newest file", which another
# session in the same folder could hijack). Fall back to newest at arm time.
SID="$CLAUDE_CODE_SESSION_ID"
F=$(ls "$HOME/.claude/projects"/*/"$SID".jsonl 2>/dev/null | head -1)
[ -z "$F" ] && F=$(ls -t "$HOME/.claude/projects/$(pwd | tr '/._' '---')"/*.jsonl | head -1)
END=$(( $(date +%s) + HOURS*3600 )); PREV=__init__; LAST=$(date +%s)
while [ $(date +%s) -lt $END ]; do
sleep 60
# Activity signal = timestamp of the last transcript line. It changes only on a
# real turn — a bare file-mtime bump (recap rewrite, atomic save) does NOT fool it.
SIG=$(tail -n 1 "$F" | grep -oE '"timestamp":"[^"]+"' | tail -1)
[ -z "$SIG" ] && SIG=$(wc -c < "$F")
[ "$SIG" != "$PREV" ] && { PREV="$SIG"; LAST=$(date +%s); } # new turn -> reset clock
IDLE=$(( $(date +%s) - LAST ))
if [ $IDLE -ge $((MINUTES*60)) ]; then
echo "HEARTBEAT idle=${IDLE}s" # wakes Claude -> tiny reply -> cache refreshed
sleep 300 # let the reply land; avoids double-fire
# Self-check: if the beat itself came back uncached, the TTL is too short to bridge.
CR=$(tail -n 400 "$F" | grep -oE '"cache_read_input_tokens":[0-9]+' | tail -1 | grep -oE '[0-9]+')
CC=$(tail -n 400 "$F" | grep -oE '"cache_creation_input_tokens":[0-9]+' | tail -1 | grep -oE '[0-9]+')
[ -n "$CC" ] && [ -n "$CR" ] && [ "$CC" -gt "$CR" ] && echo "CACHEBEAT_WARN uncached beat"
fi
done
echo CACHEBEAT_EXPIRED
Every emitted line wakes Claude; the skill instructs it to reply with a few words only, because
every extra token gets re-read by all future requests. CACHEBEAT_WARN flags a session whose cache
TTL is too short for any heartbeat to help; CACHEBEAT_EXPIRED ends it.
Why the timestamp, not the file mtime? The mtime of a
.jsonladvances whenever Claude Code rewrites the file for its own reasons (recap regeneration, atomic saves) — no turn, no cache refresh. Measuring idle from mtime therefore undercounts idle time and fires the beat late, past the TTL, so the beat lands as a full-price uncached re-read: the exact failure this is meant to prevent. The last line'stimestamponly moves on a genuine turn, so it can't be fooled.
Linux, WSL2, and macOS. The monitor uses only portable tools — tail, grep, wc, date +%s —
with no stat and no timestamp parsing, so there are no GNU-vs-BSD differences to trip over.
Native Windows (non-WSL) is untested — the monitor assumes a POSIX shell.
It reads Claude Code's session transcript at ~/.claude/projects/<slug>/<session-id>.jsonl (found
via $CLAUDE_CODE_SESSION_ID, else the newest transcript in the folder) — the default layout, but
an internal one rather than a documented API, so a future Claude Code change could require an update
here.
MIT — do whatever you like with it.