Shows plan window usage, reset times and session cost in the band above the prompt.

<img width="1096" height="808" alt="Screenshot 2026-10-19 at 8 25 29 PM" src="https://github.com/user-attachments/assets/b22cc708-5a70-4333-b3ba-79517a3b7603" />
Mods for Claude Code that add a band above the prompt, a side pane or a slash command. Each is its own plugin, so you can install only the ones you want.
| Plugin | Band | What it does | Command | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
band-rule | line | Draws a dim line above the bands to separate them from Claude's output. List it before the other mods (see below). | none | |||||||||
cache-clock | CACHE | Shows whether the prompt cache is still warm, the time left, % reused and the cold-rewrite cost, with a Compact button. Toasts shortly before the cache goes cold. | `/cache-clock on \ | off \ | status` | |||||||
usage-gauge | USAGE | Plan usage windows (5h, week, others) with reset times, the session cost and a pace warning. Toasts at 75% and 90%. | /usage-gauge (opens a pane), `on \ | off \ | warn <a> <b> \ | status` | ||||||
usage-limits | footer | Session (5h) and week (7d) rate-limit bars in the prompt footer. | none | |||||||||
auto-handoff | CONTEXT | Watches how full the chat is and saves a HANDOFF.md when it reaches a threshold, then offers to clear and resume. | `/auto-handoff on \ | off \ | now \ | resume \ | threshold <n> \ | mode auto\ | ask \ | hide \ | show \ | status` |
agent-watch | AGENTS | Shows the main chat and helper agents with their models and tasks, plus the skills that ran this turn. Opens an Agents pane. | /agent-watch (opens the Agents pane), `on \ | off \ | clear \ | help` |
Bands stack in the order their plugins appear in enabledPlugins in ~/.claude/settings.json, first on top. To reorder, move the lines and run /reload-plugins; claude plugin install adds new entries at the end. band-rule draws its line above the whole stack only when it is listed first. The author's order, top to bottom: band-rule, agent-watch, auto-handoff, cache-clock, usage-gauge, usage-limits. The last one draws in the footer, so its place doesn't matter. The Band column is the title each mod shows in its band.
Each band is one row. In a narrow terminal the end of the row is cut off with … instead of wrapping.
usage-gauge and usage-limits both show plan usage, so enable only one.
In auto mode, auto-handoff sends the handoff prompt as soon as the threshold is crossed, even while Claude is mid-task. The default ask mode only offers the button.
cache-clock has a cacheMinutes option (5 or 60, default 60). Other settings are set through each mod's commands and persist between sessions.
Install a mod from the marketplace:
/plugin install cache-clock --marketplace Abdallah-Abdelazim/claude-code-mods
Run it once per mod, changing the plugin name. From a local clone, use --marketplace ~/path/to/claude-code-mods instead.
To try one without installing: claude --plugin-dir plugins/<name>.
These mods use Claude Code's function-hook plugin API, which is in early access; they were written against Claude Code 2.1.303. Where an organization manages Claude Code, plugins you install don't run the session.compact, skill.prompt and prompt.compose hooks.
Each folder under plugins/ is a standalone mod: .claude-plugin/plugin.json, hooks/, tsconfig.json, a *.test.ts and, for a mod that keeps state, types/index.d.ts. See CLAUDE.md for the conventions and the release steps.
claude plugin validate plugins/<name>
claude plugin test plugins/<name>
The engine writes .claude-plugin/types/ inside a mod when it loads it; the folder is git-ignored.
hooks/register.tsx 239 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface as Engine, Register, SessionMeasureInput, SessionRateLimit } from 'claude-code'
3
4import type { Limit } from '../types'
5import { TRACK, bar, countdown, outIn, resetText, shortName, titleName, toneOf } from './format'
6
7const KEY = 'usage-gauge'
8const PANE = 'usage-gauge'
9const USAGE = 'Usage: /usage-gauge | on | off | warn <low> <high> | status'
10const TICK_MS = 30_000
11
12const isOnA = atom({ plugin: 'usage-gauge', key: 'isOn' } as const, true)
13const limitsA = atom({ plugin: 'usage-gauge', key: 'limits' } as const, [] as Limit[])
14const contextA = atom({ plugin: 'usage-gauge', key: 'context' } as const, null)
15const costA = atom({ plugin: 'usage-gauge', key: 'cost' } as const, null)
16const turnBaseA = atom({ plugin: 'usage-gauge', key: 'turnBase' } as const, null)
17const thresholdsA = atom({ plugin: 'usage-gauge', key: 'thresholds' } as const, { low: 75, high: 90 })
18const notifiedA = atom({ plugin: 'usage-gauge', key: 'notified' } as const, {})
19const paceA = atom({ plugin: 'usage-gauge', key: 'pace' } as const, null)
20
21const toLimits = (list: readonly SessionRateLimit[]): Limit[] =>
22 list.map(r => ({ kind: r.kind, percent: r.percentUsed, resetsAt: r.resetsAt ?? null }))
23
24async function load($: Engine) {
25 const saved = (await $.store.get(KEY)) as { isOn?: boolean; low?: number; high?: number } | null
26 await update($, isOnA, () => saved?.isOn ?? true)
27 await update($, thresholdsA, t => ({ low: saved?.low ?? t.low, high: saved?.high ?? t.high }))
28}
29
30async function save($: Engine) {
31 const { low, high } = await read($, thresholdsA)
32 await $.store.set(KEY, { isOn: await read($, isOnA), low, high })
33}
34
35async function seed($: Engine) {
36 const usage = await $.session.usage().catch(() => null)
37 if (!usage) return
38 await update($, limitsA, () => toLimits(usage.rateLimits))
39 await update($, contextA, () => usage.context.percent ?? null)
40 await update($, costA, () => usage.cost?.usd ?? null)
41}
42
43async function record($: Engine, e: SessionMeasureInput) {
44 const now = await $.clock.now()
45 const limits = toLimits(e.rateLimits)
46 await update($, limitsA, () => limits)
47 await update($, contextA, () => e.context.percent ?? null)
48 const usd = e.cost?.usd
49 if (usd !== undefined) await update($, costA, () => usd)
50
51 const five = limits.find(l => l.kind === 'five_hour')
52 const fiveReset = five?.resetsAt
53 if (five && fiveReset) {
54 const pace = await read($, paceA)
55 if (pace?.resetsAt !== fiveReset) {
56 await update($, paceA, () => ({ at: now, percent: five.percent, resetsAt: fiveReset }))
57 }
58 }
59
60 const { low, high } = await read($, thresholdsA)
61 const isOn = await read($, isOnA)
62 for (const l of limits) {
63 const level = l.percent >= high ? high : l.percent >= low ? low : null
64 const prev = (await read($, notifiedA))[l.kind]
65 if (level === null || (prev && prev.resetsAt === l.resetsAt && prev.level >= level)) continue
66 await update($, notifiedA, n => ({ ...n, [l.kind]: { resetsAt: l.resetsAt, level } }))
67 if (!isOn) continue
68 const when = resetText(l.kind, l.resetsAt, now)
69 const reset = when ? ` — resets ${l.kind === 'five_hour' ? 'in ' : ''}${when}` : ''
70 $.ui.toast(`${titleName(l.kind)} limit at ${level}%${reset}`)
71 }
72}
73
74async function startTurn($: Engine) {
75 const cost = await read($, costA)
76 await update($, turnBaseA, () => cost)
77}
78
79async function summary($: Engine) {
80 const limits = await read($, limitsA)
81 const context = await read($, contextA)
82 const cost = await read($, costA)
83 const { low, high } = await read($, thresholdsA)
84 const now = await $.clock.now()
85 const lines = limits.map(
86 l => `${shortName(l.kind)} ${Math.round(l.percent)}%${l.resetsAt ? ` · resets ${resetText(l.kind, l.resetsAt, now)}` : ''}`,
87 )
88 if (context !== null) lines.push(`context ${Math.round(context)}%`)
89 if (cost !== null) lines.push(`session $${cost.toFixed(2)}`)
90 if (lines.length === 0) lines.push('No usage data yet.')
91 lines.push(`toasts at ${low}% and ${high}%`)
92 return lines.join('\n')
93}
94
95export const register: Register = on => {
96 on('session.start', async ($, e, next) => {
97 await $.command.register({
98 name: 'usage-gauge',
99 description: 'Show plan usage, reset times and session cost.',
100 argumentHint: 'on | off | warn <low> <high> | status',
101 })
102 await load($)
103 await seed($)
104 $.clock.every(TICK_MS, () => $.ui.invalidate('ui.render'))
105
106 return next(e)
107 })
108
109 on('session.measure', async ($, e, next) => {
110 await record($, e)
111
112 return next(e)
113 })
114
115 on('turn.start', async ($, e, next) => {
116 await startTurn($)
117
118 return next(e)
119 })
120
121 on('command.run', { command: 'usage-gauge' }, async ($, e) => {
122 const [cmd = '', a = '', b = ''] = e.args.trim().split(/\s+/)
123 if (cmd === '') {
124 await $.ui.open({ id: PANE, title: 'Usage' })
125 return { text: 'Usage pane opened.' }
126 }
127 if (cmd === 'on' || cmd === 'off') {
128 await update($, isOnA, () => cmd === 'on')
129 await save($)
130 return { text: `Usage gauge ${cmd}.` }
131 }
132 if (cmd === 'warn') {
133 const low = Number(a)
134 const high = Number(b)
135 if (!(Number.isInteger(low) && Number.isInteger(high) && low >= 1 && low < high && high <= 100)) {
136 return { text: 'Usage: /usage-gauge warn <low> <high>, whole numbers 1 to 100, low below high.' }
137 }
138 await update($, thresholdsA, () => ({ low, high }))
139 await save($)
140 return { text: `Toasts at ${low}% and ${high}%.` }
141 }
142 if (cmd === 'status') return { text: await summary($) }
143
144 return { text: USAGE }
145 })
146
147 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
148 const below = await next(e)
149 if (!(await read($, isOnA))) return below
150
151 const { Box, Text } = $.ui.resolve(e)
152 const limits = await read($, limitsA)
153 const context = await read($, contextA)
154 const cost = await read($, costA)
155 const base = await read($, turnBaseA)
156 const pace = await read($, paceA)
157 const now = await $.clock.now()
158 const five = limits.find(l => l.kind === 'five_hour')
159 const out = five && pace ? outIn(pace, five.percent, now) : null
160 const delta = cost !== null && base !== null ? Math.max(0, cost - base) : null
161
162 return (
163 <Box flexDirection="column">
164 <Box>
165 <Box flexShrink={0}><Text bold color="#0b0b0f" backgroundColor="#e0af68"> ◔ USAGE </Text></Box>
166 <Text wrap="truncate-end">
167 {limits.map((l, i) => {
168 const cells = bar(l.percent, 6)
169 return (
170 <Text color={toneOf(l.percent)}>
171 {i ? ' │ ' : ' '}
172 {shortName(l.kind)} {cells.on}<Text color={TRACK}>{cells.off}</Text>{' '}{Math.round(l.percent)}%
173 {l.resetsAt ? ` ↻${resetText(l.kind, l.resetsAt, now)}` : ''}
174 </Text>
175 )
176 })}
177 {out !== null && <Text color="#f59e0b">{' ⚠ at this pace: out in '}{countdown(out)}</Text>}
178 {limits.length === 0 && context !== null && (
179 <Text color={toneOf(context)}>
180 {' context '}{bar(context, 6).on}<Text color={TRACK}>{bar(context, 6).off}</Text>{' '}{Math.round(context)}%
181 </Text>
182 )}
183 {cost !== null && (
184 <Text dimColor>
185 {' │ session $'}
186 {cost.toFixed(2)}
187 {delta !== null ? ` (+$${delta.toFixed(2)})` : ''}
188 </Text>
189 )}
190 {limits.length === 0 && context === null && cost === null && (
191 <Text dimColor>{' no usage data yet'}</Text>
192 )}
193 </Text>
194 </Box>
195 {below}
196 </Box>
197 )
198 })
199
200 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
201 const { Box, Text } = $.ui.resolve(e)
202 const limits = await read($, limitsA)
203 const context = await read($, contextA)
204 const cost = await read($, costA)
205 const base = await read($, turnBaseA)
206 const now = await $.clock.now()
207 const cells = Math.max(10, Math.min(60, e.props.bodyColumns - 34))
208 const delta = cost !== null && base !== null ? Math.max(0, cost - base) : null
209
210 return (
211 <Box flexDirection="column">
212 {limits.map(l => {
213 const row = bar(l.percent, cells)
214 return (
215 <Text color={toneOf(l.percent)}>
216 {shortName(l.kind).padEnd(7)} {row.on}<Text color={TRACK}>{row.off}</Text>{' '}{Math.round(l.percent)}%
217 {l.resetsAt ? ` ↻ ${resetText(l.kind, l.resetsAt, now)}` : ''}
218 </Text>
219 )
220 })}
221 {context !== null && (
222 <Text color={toneOf(context)}>
223 {'context'.padEnd(7)} {bar(context, cells).on}<Text color={TRACK}>{bar(context, cells).off}</Text>{' '}{Math.round(context)}%
224 </Text>
225 )}
226 {cost !== null && (
227 <Text dimColor>
228 {'session'.padEnd(7)} ${cost.toFixed(2)}
229 {delta !== null ? ` (+$${delta.toFixed(2)})` : ''}
230 </Text>
231 )}
232 {limits.length === 0 && context === null && cost === null && (
233 <Text dimColor>No usage data yet.</Text>
234 )}
235 </Box>
236 )
237 })
238}
239hooks/format.ts 50 lines1import type { Pace } from '../types'
2
3const SHORT: Record<string, string> = { five_hour: '5h', seven_day: 'week' }
4const TITLE: Record<string, string> = { five_hour: '5-hour', seven_day: 'Weekly' }
5const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
6const PACE_MIN_MS = 5 * 60_000
7
8export const shortName = (kind: string) => SHORT[kind] ?? kind
9export const titleName = (kind: string) => TITLE[kind] ?? kind
10
11// Green under 60%, amber 60 to 85, red over 85.
12export const toneOf = (pct: number) => (pct > 85 ? '#ef4444' : pct >= 60 ? '#f59e0b' : '#22c55e')
13
14// The empty part is the same block in TRACK gray, drawn as its own Text run.
15export const TRACK = '#52525b'
16
17export function bar(pct: number, cells: number) {
18 const on = Math.max(0, Math.min(cells, Math.round((cells * pct) / 100)))
19 return { on: '▆'.repeat(on), off: '▆'.repeat(cells - on) }
20}
21
22export function countdown(ms: number) {
23 const m = Math.max(0, Math.ceil(ms / 60_000))
24 const h = Math.floor(m / 60)
25 return h ? `${h}h${m % 60}m` : `${m}m`
26}
27
28export function dayTime(iso: string) {
29 const d = new Date(iso)
30 const h = d.getHours()
31 const min = d.getMinutes() ? `:${String(d.getMinutes()).padStart(2, '0')}` : ''
32 return `${DAYS[d.getDay()]} ${h % 12 || 12}${min}${h < 12 ? 'am' : 'pm'}`
33}
34
35// The five-hour window counts down; longer windows name their reset day.
36export function resetText(kind: string, iso: string | null, now: number) {
37 if (!iso) return ''
38 return kind === 'five_hour' ? countdown(Date.parse(iso) - now) : dayTime(iso)
39}
40
41// Ms until the window runs out at its current rate, if that comes before it resets.
42export function outIn(pace: Pace, percent: number, now: number) {
43 const elapsed = now - pace.at
44 const gained = percent - pace.percent
45 const resetAt = Date.parse(pace.resetsAt)
46 if (elapsed < PACE_MIN_MS || gained <= 0 || resetAt <= now) return null
47 const ms = ((100 - percent) * elapsed) / gained
48 return now + ms < resetAt ? ms : null
49}
50types/index.d.ts 19 lines1export type Limit = { kind: string; percent: number; resetsAt: string | null }
2export type Notice = { resetsAt: string | null; level: number }
3export type Pace = { at: number; percent: number; resetsAt: string }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'usage-gauge': {
8 isOn: boolean
9 limits: Limit[]
10 context: number | null
11 cost: number | null
12 turnBase: number | null
13 thresholds: { low: number; high: number }
14 notified: Record<string, Notice>
15 pace: Pace | null
16 }
17 }
18}
19