Warns before a prompt that would reload a cold prompt cache, with a token and cost estimate. /keepwarm pings the session before the cache goes cold.

Nine mods for Claude Code (2.1.287 or newer), written from the walkthrough video. Each lives in mods/<name>.
Install one, or all, from a terminal session:
/plugin install <name> --marketplace KianaVega/KianaVega
| Mod | What it does | Extra model usage |
|---|---|---|
| savvy-progress | Progress bar above the prompt and a live list of subagents with model, tokens and rough cost | none |
| tool-skins | Icons and colors for tool calls in seven themes, switch with /skin <name> | none |
| you-should-know | A second, small model reads long answers and flags anything important you might miss | one small call per long answer |
| file-tree | Pane showing files Claude reads and edits, green once committed (/file-tree) | none |
| cache-tax | Holds a prompt that would reload a cold cache and shows an estimate. /keepwarm pings before the cache expires | none, except keep-warm pings |
| blast-radius | Stops recursive deletes, git clean, hard resets and shows the affected files before you press Proceed | none |
| reflect | Offers to save corrections as a rule in CLAUDE.md | one small call when you save |
| terminal-browser | /browse <url or file> opens a pane with a screenshot (kitty or Ghostty) and the page text | none |
| replay-theater | Replay button after a turn that steps through each edit as a diff (/replay) | none |
hooks/register.tsx. Edit and reload.hooks/register.tsx 99 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4// The cache lives about an hour on a subscription plan (5 minutes on the plain API).
5const CACHE_TTL_MS = 60 * 60 * 1000
6const WARN_AFTER_MS = 55 * 60 * 1000 // ping this long after the last request
7const KEEP_WARM_BY_DEFAULT = false
8const MIN_TOKENS_TO_WARN = 20_000
9const INPUT_USD_PER_MTOK = 3 // rough price for reloading uncached context
10const CACHE_WRITE_MULTIPLIER = 1.25
11
12const warning = atom({ plugin: 'cache-tax', key: 'warning' } as const, null as string | null)
13
14let lastActivity: number | undefined
15let isKeepingWarm = KEEP_WARM_BY_DEFAULT
16let isHeld = false
17let timer: { cancel: () => void } | undefined
18
19function arm($: EngineInterface) {
20 timer?.cancel()
21 if (!isKeepingWarm) return
22 timer = $.clock.after(WARN_AFTER_MS, async () => {
23 // A tiny message so the cache entry is read, and renewed, before it expires.
24 await $.prompt.submit({ text: 'Reply with the single word: warm.' })
25 })
26}
27
28export const register: Register = on => {
29 lastActivity = undefined
30 isHeld = false
31
32 on('session.start', async ($, e, next) => {
33 lastActivity = await $.clock.now()
34 await $.command.register({ name: 'keepwarm', description: 'Toggle pinging the session to keep the prompt cache warm' })
35 arm($)
36
37 return next(e)
38 })
39
40 on('command.run', { command: 'keepwarm' }, async $ => {
41 isKeepingWarm = !isKeepingWarm
42 arm($)
43
44 return { text: `Keep-warm is ${isKeepingWarm ? 'on' : 'off'}.` }
45 })
46
47 // Change: hold a prompt that would reload a cold cache until the person sends it again.
48 on('prompt.submit', async ($, e, next) => {
49 const now = await $.clock.now()
50 const idle = lastActivity === undefined ? 0 : now - lastActivity
51 const isCold = lastActivity !== undefined && idle > CACHE_TTL_MS
52
53 if (isCold && !isHeld) {
54 const usage = await $.session.usage()
55 const tokens = usage.context.tokens ?? 0
56 if (tokens === 0 || tokens >= MIN_TOKENS_TO_WARN) {
57 const cost = (tokens / 1_000_000) * INPUT_USD_PER_MTOK * CACHE_WRITE_MULTIPLIER
58 const size = tokens === 0 ? 'the whole conversation' : `about ${tokens.toLocaleString()} tokens`
59 const text = `Cache is cold (${Math.round(idle / 60000)} min idle). Sending reloads ${size}${tokens === 0 ? '' : `, roughly $${cost.toFixed(2)}`}. Send again to continue.`
60 isHeld = true
61 await update($, warning, () => text)
62 $.ui.toast(text, { timeoutMs: 20000 })
63
64 await $.prompt.fill({ text: e.text, mode: 'replace' })
65
66 return { drop: text }
67 }
68 }
69
70 isHeld = false
71 lastActivity = now
72 await update($, warning, () => null)
73 arm($)
74
75 return next(e)
76 })
77
78 on('turn.complete', async ($, e, next) => {
79 lastActivity = await $.clock.now()
80 arm($)
81
82 return next(e)
83 })
84
85 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
86 const text = await read($, warning)
87 if (e.props.hasSurvey || text === null) {
88 return next(e)
89 }
90 const { Box, Text } = $.ui.resolve(e)
91
92 return (
93 <Box>
94 <Text color="warning">{text}</Text>
95 </Box>
96 )
97 })
98}
99types/index.d.ts 8 lines1export type Warning = string | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'cache-tax': { warning: Warning }
6 }
7}
8