SLOPSHOPPER

cache-watch

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

newtoast
v0.1.0no licenseupdated 2026-10-02anthonyhungnguyen/claude-code-mods/cache-watch
A shopper browsing a rack in a slop shop
README

claude-code-mods

Mods for Claude Code, one folder each. A mod is a plugin of function hooks: panes, status lines, slash commands, tool guards.

ModWhat it does
cost-paneToken and USD spend per session and per day, in the status line and a /cost-pane pane
context-guardContext window fill in the status line; a toast once it passes 80%
cache-watchA toast when a turn rebuilt the prompt cache: tokens written, turn cost, likely cause (idle, model change)
turn-timerSpeaks (say on macOS) and toasts when a turn longer than 2 minutes finishes

Install

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>.

Source 2 files
hooks/register.ts 53 lines
1import { 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}
53
types/index.d.ts 8 lines
1export type Millis = number
2
3declare module 'claude-code' {
4  interface PluginState {
5    'cache-watch': { lastEnd: Millis; lastModel: string; lastUsd: number }
6  }
7}
8