A band above the prompt with your Claude plan usage: 5-hour and 7-day bars with reset countdowns and a pace marker, plus the context window. No network calls…

A Claude Code mod that keeps your plan usage in a band above the prompt:
Usage 5h ▰▰▱▱▱│▱▱▱▱ 16% 2h13m · 7d ▰▰▰▰▰│▱▱▱▱ 48% 3d4h · context ▰▰▱▱▱▱▱▱▱▱ 22%
<img width="640" height="36" alt="image" src="https://github.com/user-attachments/assets/210d3bf7-f1a4-4a78-910a-8c2a4c98fa56" />
│ pace marker: where usage would be if you spread it evenly through the window. Filled cells past the marker mean you are using it faster than that.The figures are the ones the session itself reports (the same ones Claude Code's status line gets), so the mod makes no network calls and spends no tokens. Usage arrives with a session's first response; until then the band shows the last reading from an earlier session, dimmed. On an API key, with no plan limits, it shows the context bar alone. On a narrow terminal the bars drop out and the numbers stay. Collapse the band with ctrl+x ctrl+a.
Set in /config (or pluginConfigs in settings):
| Option | Default | What |
|---|---|---|
warnAt | 70 | A bar turns amber at this % |
criticalAt | 90 | A bar turns red at this %, and 5h / 7d toast once when they cross it |
showPace | true | The pace marker in the 5h / 7d bars |
showContext | true | The context bar |
toast | true | The toast near the limit |
CLAUDE_CONFIG_DIR) in the mod's own store, so several accounts on one machine don't mix.At the prompt of a Claude Code terminal session:
/plugin install usage-band --marketplace wagnerporras/claude-mods
claude --plugin-dir <path to this folder>
Tests: claude plugin test <folder>; checks: claude plugin validate <folder>.
hooks/register.tsx 222 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit, ThemeKey } from 'claude-code'
3
4import type { Reading, UsageWindow } from '../types'
5
6// A band above the prompt with the session's plan usage: 5h and 7d windows
7// (bar, pace marker, percent, reset countdown) and the context window. Reads
8// only what the session reports; the last reading is kept per account so a new
9// session starts with it.
10
11const CELLS = 10
12// Below this many columns the bars drop out; the full line is about 82 wide.
13const NARROW = 88
14const WINDOWS = [
15 { kind: 'five_hour', label: '5h', ms: 5 * 3_600_000 },
16 { kind: 'seven_day', label: '7d', ms: 7 * 86_400_000 },
17] as const
18
19const reading = atom({ plugin: 'usage-band', key: 'reading' } as const, null)
20
21let config = { warnAt: 70, criticalAt: 90, showPace: true, showContext: true, toast: true }
22let tick: { cancel: () => void } | undefined
23
24export function filled(percent: number): number {
25 return Math.min(CELLS, Math.max(0, Math.round(percent / (100 / CELLS))))
26}
27
28export function level(percent: number, warnAt = 70, criticalAt = 90): ThemeKey {
29 return percent >= criticalAt ? 'error' : percent >= warnAt ? 'warning' : 'success'
30}
31
32// The cell an even pace through the window has reached, or undefined.
33export function paceCell(resetsAt: string | undefined, windowMs: number, now: number): number | undefined {
34 const end = resetsAt ? Date.parse(resetsAt) : NaN
35 if (Number.isNaN(end)) return undefined
36 const elapsed = Math.min(1, Math.max(0, 1 - (end - now) / windowMs))
37 return Math.min(CELLS - 1, Math.floor(elapsed * CELLS))
38}
39
40// Time left as its two largest units: 45m, 2h13m, 3d4h.
41export function countdown(ms: number): string {
42 const minutes = Math.max(0, Math.round(ms / 60_000))
43 const days = Math.floor(minutes / 1440)
44 const hours = Math.floor((minutes % 1440) / 60)
45 const mins = minutes % 60
46 if (days > 0) return `${days}d${hours}h`
47 if (hours > 0) return `${hours}h${String(mins).padStart(2, '0')}m`
48 return `${mins}m`
49}
50
51// The bar as runs of filled, empty and pace cells.
52export function runs(percent: number, pace?: number): { text: string; kind: 'fill' | 'empty' | 'pace' }[] {
53 const full = filled(percent)
54 const out: { text: string; kind: 'fill' | 'empty' | 'pace' }[] = []
55 for (let i = 0; i < CELLS; i++) {
56 const kind = i === pace ? 'pace' : i < full ? 'fill' : 'empty'
57 const text = kind === 'pace' ? '│' : kind === 'fill' ? '▰' : '▱'
58 const last = out[out.length - 1]
59 if (last && last.kind === kind) last.text += text
60 else out.push({ text, kind })
61 }
62 return out
63}
64
65const toWindows = (limits: readonly SessionRateLimit[]): UsageWindow[] =>
66 limits.flatMap(l =>
67 l.kind === 'five_hour' || l.kind === 'seven_day'
68 ? [{ kind: l.kind, percent: l.percentUsed, resetsAt: l.resetsAt }]
69 : [],
70 )
71
72const isCurrent = (w: UsageWindow, now: number) => !w.resetsAt || Date.parse(w.resetsAt) > now
73
74async function account($: EngineInterface): Promise<string> {
75 return (await $.env.get('CLAUDE_CONFIG_DIR')) ?? 'default'
76}
77
78async function remember($: EngineInterface, windows: UsageWindow[]): Promise<void> {
79 if (windows.length) await $.store.set(`last:${await account($)}`, windows)
80}
81
82async function recall($: EngineInterface, now: number): Promise<UsageWindow[]> {
83 const kept = (await $.store.get(`last:${await account($)}`)) as UsageWindow[] | undefined
84 return (kept ?? []).filter(w => isCurrent(w, now))
85}
86
87// Toasts once per window when it crosses the red threshold.
88async function warn($: EngineInterface, windows: UsageWindow[], now: number): Promise<void> {
89 if (!config.toast) return
90 for (const w of windows) {
91 if (w.percent < config.criticalAt) continue
92 const key = `toasted:${await account($)}:${w.kind}`
93 if ((await $.store.get(key)) === (w.resetsAt ?? 'unknown')) continue
94 await $.store.set(key, w.resetsAt ?? 'unknown')
95 const label = w.kind === 'five_hour' ? '5-hour' : '7-day'
96 const left = w.resetsAt ? `, resets in ${countdown(Date.parse(w.resetsAt) - now)}` : ''
97 $.ui.toast(`${label} usage at ${Math.round(w.percent)}%${left}`)
98 }
99}
100
101async function measured($: EngineInterface, limits: readonly SessionRateLimit[], context?: number): Promise<void> {
102 const now = await $.clock.now()
103 const windows = toWindows(limits)
104 const prior = await read($, reading)
105 await update($, reading, () =>
106 windows.length
107 ? { windows, context: context ?? prior?.context, isStale: false, now }
108 : { windows: prior?.windows ?? [], context: context ?? prior?.context, isStale: prior?.isStale ?? true, now },
109 )
110 if (windows.length) {
111 await remember($, windows)
112 await warn($, windows, now)
113 }
114}
115
116async function start($: EngineInterface): Promise<void> {
117 const now = await $.clock.now()
118 const usage = await $.session.usage()
119 const live = toWindows(usage.rateLimits)
120 const windows = live.length ? live : await recall($, now)
121 await update($, reading, () => ({
122 windows,
123 context: usage.context.percent,
124 isStale: live.length === 0,
125 now,
126 }))
127 if (live.length) {
128 await remember($, live)
129 await warn($, live, now)
130 }
131}
132
133async function clock($: EngineInterface): Promise<void> {
134 const now = await $.clock.now()
135 await update($, reading, r => (r ? { ...r, now, windows: r.windows.filter(w => isCurrent(w, now)) } : r))
136}
137
138export const register: Register = (on, options) => {
139 config = {
140 warnAt: Number(options.warnAt ?? 70),
141 criticalAt: Number(options.criticalAt ?? 90),
142 showPace: options.showPace !== false,
143 showContext: options.showContext !== false,
144 toast: options.toast !== false,
145 }
146
147 on('session.start', async ($, e, next) => {
148 const started = await next(e)
149 try {
150 await start($)
151 } catch {
152 // Nothing to show until the session reports usage.
153 }
154 tick?.cancel()
155 // Countdowns and pace markers move with the clock.
156 tick = $.clock.every(60_000, () => void clock($).catch(() => undefined))
157 return started
158 })
159
160 on('session.measure', async ($, e, next) => {
161 if (e.changed.includes('rateLimits') || e.changed.includes('context')) {
162 await measured($, e.rateLimits, e.context.percent).catch(() => undefined)
163 }
164 return next(e)
165 })
166
167 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
168 const r = await read($, reading)
169 const context = config.showContext ? r?.context : undefined
170 if (e.props.hasSurvey || !r || (r.windows.length === 0 && context === undefined)) return next(e)
171
172 const { Box, Text } = $.ui.resolve(e)
173 const isNarrow = e.props.bodyColumns < NARROW
174
175 const bar = (percent: number, pace?: number) =>
176 isNarrow ? null : (
177 <Text>
178 {runs(percent, config.showPace ? pace : undefined).map(run =>
179 run.kind === 'fill' ? (
180 <Text color={level(percent, config.warnAt, config.criticalAt)}>{run.text}</Text>
181 ) : run.kind === 'pace' ? (
182 <Text bold>{run.text}</Text>
183 ) : (
184 <Text dimColor>{run.text}</Text>
185 ),
186 )}{' '}
187 </Text>
188 )
189
190 const parts = WINDOWS.flatMap(spec => {
191 const w = r.windows.find(x => x.kind === spec.kind)
192 if (!w) return []
193 const left = w.resetsAt ? Date.parse(w.resetsAt) - r.now : NaN
194 return [
195 <Text>
196 {spec.label} {bar(w.percent, paceCell(w.resetsAt, spec.ms, r.now))}
197 <Text dimColor={r.isStale}>{Math.round(w.percent)}%</Text>
198 {Number.isNaN(left) ? '' : <Text dimColor> {countdown(left)}</Text>}
199 </Text>,
200 ]
201 })
202 if (context !== undefined) {
203 parts.push(
204 <Text>
205 context {bar(context)}
206 {Math.round(context)}%
207 </Text>,
208 )
209 }
210
211 return (
212 <Box>
213 <Text>
214 <Text bold>Usage</Text>
215 {' '}
216 {parts.flatMap((part, i) => (i ? [<Text dimColor> · </Text>, part] : [part]))}
217 </Text>
218 </Box>
219 )
220 })
221}
222types/index.d.ts 22 lines1// One rate-limit window as the session reported it.
2export type UsageWindow = {
3 kind: 'five_hour' | 'seven_day'
4 percent: number
5 resetsAt?: string
6}
7
8// What the band draws. `isStale`: the windows are the last reading kept from an
9// earlier session, until this session's first response reports its own.
10export type Reading = {
11 windows: UsageWindow[]
12 context?: number
13 isStale: boolean
14 now: number
15}
16
17declare module 'claude-code' {
18 interface PluginState {
19 'usage-band': { reading: Reading | null }
20 }
21}
22