Toast when a turn rebuilt the prompt cache, with what it cost and the likely cause

Mods for Claude Code, one folder each. A mod is a plugin of function hooks: panes, status lines, slash commands, tool guards.
| Mod | What it does |
|---|---|
| cost-pane | Token and USD spend per session and per day, in the status line and a /cost-pane pane |
| context-guard | Context window fill in the status line; a toast once it passes 80% |
| cache-watch | A toast when a turn rebuilt the prompt cache: tokens written, turn cost, likely cause (idle, model change) |
| turn-timer | Speaks (say on macOS) and toasts when a turn longer than 2 minutes finishes |
git clone git@github.com:anthonyhungnguyen/claude-code-mods.git ~/.claude/mods
List each mod folder you want in ~/.claude/settings.json, separated by ::
{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/cost-pane:~/.claude/mods/context-guard:~/.claude/mods/cache-watch:~/.claude/mods/turn-timer" } }
New sessions load them. Check a mod with claude plugin validate ~/.claude/mods/<mod>.
hooks/register.ts 53 lines1import { atom, read, update } from 'claude-code'
2import type { ModelUsage, Register } from 'claude-code'
3
4const MIN_REBUILD = 20_000
5const lastEndA = atom({ plugin: 'cache-watch', key: 'lastEnd' } as const, 0)
6const lastModelA = atom({ plugin: 'cache-watch', key: 'lastModel' } as const, '')
7const lastUsdA = atom({ plugin: 'cache-watch', key: 'lastUsd' } as const, 0)
8
9// a rebuild writes most of the prompt to the cache instead of reading it back;
10// normal turns only write the few new messages
11export const isRebuild = (u: ModelUsage) =>
12 u.cache_creation_input_tokens >= MIN_REBUILD &&
13 u.cache_creation_input_tokens > u.cache_read_input_tokens
14
15const tok = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1e3)}k`)
16
17export const register: Register = on => {
18 on('session.start', async ($, e, next) => {
19 const done = await next(e)
20 const usd = (await $.session.usage()).cost?.usd ?? 0
21 await update($, lastUsdA, last => Math.max(last, usd))
22 return done
23 })
24
25 on('turn.complete', async ($, e, next) => {
26 const done = await next(e)
27 if (e.agentId || !e.usage) return done
28
29 const now = await $.clock.now()
30 const usd = (await $.session.usage()).cost?.usd ?? 0
31 let turnUsd = 0
32 await update($, lastUsdA, last => ((turnUsd = Math.max(0, usd - last)), Math.max(usd, last)))
33 const lastEnd = await read($, lastEndA)
34 const lastModel = await read($, lastModelA)
35 await update($, lastEndA, () => now)
36 await update($, lastModelA, () => e.usage?.model ?? '')
37
38 if (isRebuild(e.usage)) {
39 // gap between the last turn's end and this turn's start
40 const idleMin = lastEnd ? Math.round((now - e.durationMs - lastEnd) / 60_000) : 0
41 const cause =
42 lastModel && lastModel !== e.usage.model ? 'model changed'
43 : idleMin >= 5 ? `idle ${idleMin}m`
44 : lastEnd ? 'cause unknown'
45 : 'first turn'
46 $.ui.toast(
47 `Cache rebuilt: ${tok(e.usage.cache_creation_input_tokens)} tokens written (${cause}), turn cost $${turnUsd.toFixed(2)}`,
48 )
49 }
50 return done
51 })
52}
53types/index.d.ts 8 lines1export type Millis = number
2
3declare module 'claude-code' {
4 interface PluginState {
5 'cache-watch': { lastEnd: Millis; lastModel: string; lastUsd: number }
6 }
7}
8