Status line with prompt-cache hit rate, context size and TTL countdown; toasts and prices every cache rebuild and names what coincided with it; /cache-meter…

Two Claude Code mods — plugins of function hooks that run inside Claude Code (terminal and the Desktop app's Code tab). Needs Claude Code 2.1.287 or later (claude --version).
| Mod | What it does |
|---|---|
| blast-guard | Holds risky shell commands (rm -r, Remove-Item -Recurse, force push, git reset --hard, git clean -f, git checkout ., git branch -D) and asks Cancel / Run it, with a dry-run report of what would be lost. Blocks Python heredocs carrying \1 / \x.. escapes (they write control characters into files). Refuses a 5th concurrent subagent. |
| cache-meter | Status line: prompt-cache hit rate and the 1-hour TTL countdown (context size lives in usage-bar). A toast whenever a request rebuilds the cache instead of reading it, with the token count, the extra cost, and what changed just before (model switch, effort change, idle past the TTL, or a prompt-prefix change). /cache-meter prints the session summary. |
| usage-bar | One slim row above the prompt with solid bars for context fill and your 5-hour and 7-day plan limits (percent used, time to reset), plus the session cost. Bars are green under 60%, amber from 60%, red from 85%; at 90% the label turns into a bold red ⚠ warning and a toast fires once per reset window. more adds plain sentences (used, left, resets); × hides it. While subagents run, the row adds agent dots (pulsing while running, ✓ when done) and an agents button for savvy-progress's /agents-info. /usage-bar hides/shows, /usage-bar details expands and refreshes. |
In a Claude Code terminal session:
/plugin install blast-guard --marketplace satyamk4517/claude-mods
/plugin install cache-meter --marketplace satyamk4517/claude-mods
/plugin install usage-bar --marketplace satyamk4517/claude-mods
Answer y to add the marketplace, then pick a scope (user = every project).
Mods run with your permissions and are not sandboxed. Read the two hook files first, or list what they do without running them:
claude plugin validate ./blast-guard
claude plugin validate ./cache-meter
claude plugin validate ./usage-bar
blast-guard calls $.process.run (git dry runs only), $.ui.ask and $.agent.list. cache-meter calls the clock, status line, toasts and registers one command; it makes no network or file calls. usage-bar calls $.session.usage (the status line's own figures, no network request), $.agent.list, the clock, its store and toasts, and runs /agents-info only when you press agents.
PRICES in cache-meter/hooks/register.ts if your rates differ.$(...) or a script file walks past it. Pair it with deny permission rules for anything that must never run./cost totals, at API prices; on a subscription treat it as relative weight.claude plugin test ./blast-guard, claude plugin test ./cache-meter and claude plugin test ./usage-bar.hooks/register.ts 135 lines1import type { ModelUsage, Register } from 'claude-code'
2
3// USD per million tokens (input, output), API list prices as of 2026-09-29.
4// Models not listed get token counts only, never a guessed price.
5const PRICES: { match: RegExp; input: number; output: number }[] = [
6 { match: /opus/i, input: 4, output: 20 },
7 { match: /sonnet/i, input: 2, output: 10 },
8]
9// Prompt-cache multipliers on the input price: a 1-hour write costs 2x,
10// a read 0.1x. Assumes the session's requests use the 1-hour cache TTL.
11const WRITE_X = 2
12const READ_X = 0.1
13const TTL_MS = 60 * 60 * 1000
14// A step counts as a rebuild when it read back less than half of what the
15// loop had cached and wrote at least this much afresh.
16const MIN_REBUILD = 2000
17
18type Loop = { model: string; effort?: string | number; at: number; cached: number }
19type Rebuild = { at: number; loop: string; tokens: number; cost: number | null; why: string }
20
21const priceOf = (model: string) => PRICES.find(p => p.match.test(model)) ?? null
22
23const costOf = (model: string, u: ModelUsage) => {
24 const p = priceOf(model)
25 if (!p) return null
26 return (
27 (u.input_tokens * p.input +
28 u.cache_creation_input_tokens * p.input * WRITE_X +
29 u.cache_read_input_tokens * p.input * READ_X +
30 u.output_tokens * p.output) /
31 1e6
32 )
33}
34
35const k = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n))
36const usd = (n: number | null) => (n === null ? 'n/a' : 'USD ' + n.toFixed(2))
37const hhmm = (ms: number) => new Date(ms).toTimeString().slice(0, 5)
38
39export const register: Register = on => {
40 const loops = new Map<string, Loop>()
41 const rebuilds: Rebuild[] = []
42 const total = { steps: 0, input: 0, read: 0, write: 0, output: 0, cost: 0, unpriced: 0 }
43 let mainContext = 0
44 let mainAt = 0
45
46 const hitRate = () => {
47 const all = total.input + total.read + total.write
48 return all === 0 ? 0 : Math.round((100 * total.read) / all)
49 }
50 const tax = () => rebuilds.reduce((s, r) => s + (r.cost ?? 0), 0)
51
52 // The status line's text at time `now`; undefined before the first request.
53 const statusText = (now: number) => {
54 if (total.steps === 0) return undefined
55 const left = Math.max(0, TTL_MS - (now - mainAt))
56 const ttl = left === 0 ? 'cache cold' : `TTL ${Math.ceil(left / 60000)}m`
57 const rb = rebuilds.length ? ` · tax ${usd(tax())} (${rebuilds.length} rebuild${rebuilds.length > 1 ? 's' : ''})` : ''
58 return `cache ${hitRate()}% · ${ttl}${rb}`
59 }
60
61 on('session.start', async ($, e, next) => {
62 await $.command.register({ name: 'cache-meter', description: 'Prompt-cache spend for this session: hit rate, cost, and every rebuild with what coincided with it.' })
63 $.clock.every(60_000, () => $.ui.status(statusText(Date.now())))
64 return next(e)
65 })
66
67 on('turn.step', async function* ($, e, next) {
68 const res = yield* next(e)
69 const u = res.usage
70 if (!u) return res
71
72 const key = e.agentId ?? 'main'
73 const now = await $.clock.now()
74 const prev = loops.get(key)
75 const cost = costOf(u.model, u)
76
77 total.steps += 1
78 total.input += u.input_tokens
79 total.read += u.cache_read_input_tokens
80 total.write += u.cache_creation_input_tokens
81 total.output += u.output_tokens
82 if (cost === null) total.unpriced += 1
83 else total.cost += cost
84
85 if (prev && u.cache_read_input_tokens < prev.cached / 2 && u.cache_creation_input_tokens >= MIN_REBUILD) {
86 const why: string[] = []
87 if (prev.model !== e.model) why.push(`model ${prev.model} → ${e.model}`)
88 if (prev.effort !== e.effort) why.push(`effort ${prev.effort ?? 'default'} → ${e.effort ?? 'default'}`)
89 if (now - prev.at > TTL_MS) why.push(`idle ${Math.round((now - prev.at) / 60000)}m, past the 1h TTL`)
90 if (why.length === 0) why.push('prompt prefix changed (CLAUDE.md/memory edit, tools or MCP change, or compaction)')
91 const p = priceOf(u.model)
92 const rebuild: Rebuild = {
93 at: now,
94 loop: key === 'main' ? 'main' : `agent ${key.slice(0, 8)}`,
95 tokens: u.cache_creation_input_tokens,
96 // The tax is the rewrite premium over reading the same tokens back.
97 cost: p ? (u.cache_creation_input_tokens * p.input * (WRITE_X - READ_X)) / 1e6 : null,
98 why: why.join('; '),
99 }
100 rebuilds.push(rebuild)
101 $.ui.toast(`Cache rebuilt: ${k(rebuild.tokens)} tokens, tax ${usd(rebuild.cost)}. Coincided with: ${rebuild.why}`, { timeoutMs: 8000 })
102 }
103
104 loops.set(key, {
105 model: e.model,
106 effort: e.effort,
107 at: now,
108 cached: u.cache_read_input_tokens + u.cache_creation_input_tokens,
109 })
110 if (key === 'main') {
111 mainContext = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
112 mainAt = now
113 }
114 $.ui.status(statusText(now))
115 return res
116 })
117
118 on('command.run', { command: 'cache-meter' }, async () => {
119 if (total.steps === 0) return { text: 'cache-meter: no model requests yet this session.' }
120 const lines = [
121 `Requests: ${total.steps} · cache hit ${hitRate()}% · context now ${k(mainContext)}`,
122 `Tokens: ${k(total.read)} read from cache, ${k(total.write)} written, ${k(total.input)} uncached, ${k(total.output)} output`,
123 `Estimated spend: ${usd(total.cost)}${total.unpriced ? ` (+${total.unpriced} requests on unpriced models)` : ''} · rebuild tax ${usd(tax())}`,
124 ]
125 if (rebuilds.length) {
126 lines.push('Rebuilds:')
127 for (const r of rebuilds) lines.push(` ${hhmm(r.at)} ${r.loop} ${k(r.tokens)} tokens ${usd(r.cost)} ${r.why}`)
128 } else {
129 lines.push('No cache rebuilds so far.')
130 }
131 lines.push('Prices assume API list rates and a 1h cache (write 2x, read 0.1x); on a subscription read them as relative weight, not a bill.')
132 return { text: lines.join('\n') }
133 })
134}
135