Always-visible Claude Code usage limits: 5-hour and weekly percentage with reset countdown, plus one-click new chat and push buttons. A Claude Code mod for the…

See your Claude Code 5-hour and weekly usage limits at all times, get one-click next-step suggestions after every reply, and open a new chat or push to GitHub in one click. Free, open-source Claude Code mods for the Claude Desktop app (Code tab) and the Claude Code terminal.
Website: https://pawandeepdhall.github.io/claude-mods/
▶ Watch the 37-second demo (with voice-over)

Install in 30 seconds:
claude plugin marketplace add pawandeepdhall/claude-mods
claude plugin install usage-band@my-claude-mods
claude plugin install next-steps@my-claude-mods
Stop guessing how close you are to your Claude limit or running /usage again and again. usage-band pins a small widget above the prompt that shows:
It updates on its own, works on Pro and Max plans, shows Team and Enterprise spend limits, and hides itself on API-key accounts, where there are no limits to show. It follows your light or dark theme.
Not sure what to ask next? After every reply, next-steps shows up to 6 suggested next steps in two columns on the left of the band, such as Add tests for the parser or Deploy to staging.
SEND_MODEL to 'sonnet' in hooks/register.tsx for more detail), and identical requests are cached for 30 minutes so a repeat costs nothing/mod-bench times the mod's storage and model calls and writes the results to bench.jsonClaude Code v2.1.287 or later (mods are built in from that version). The Claude Desktop app updates itself. In a terminal, check with claude --version and update with:
npm install -g @anthropic-ai/claude-code@latest
Run these once per computer (no GitHub account needed):
claude plugin marketplace add pawandeepdhall/claude-mods
claude plugin install usage-band@my-claude-mods
claude plugin install next-steps@my-claude-mods
Or inside a Claude Code session: /plugin marketplace add pawandeepdhall/claude-mods, then /plugin install usage-band@my-claude-mods. In a session that was already open, run /reload-plugins.
claude plugin marketplace update my-claude-mods
claude plugin update usage-band@my-claude-mods
claude plugin update next-steps@my-claude-mods
Mods run with your permissions, so here is exactly what each one does. Check it yourself with claude plugin validate ./plugins/<mod>.
$.session.usage), checks whether the folder is a git repo (git rev-parse), runs a one-line script that presses Ctrl+N when you click +, and submits a commit-and-push request to Claude when you click ↑. Nothing is sent anywhere else.$.model.complete): to Haiku to write the suggestions and, when you press Send, the message it then submits as yours. /mod-bench writes timing results to a local bench.json file. Nothing is sent anywhere else.How do I see my Claude Code usage limit? Install usage-band. Your 5-hour and weekly usage show above the prompt in every session.
When does my Claude 5-hour limit reset? usage-band shows a live countdown (↻) next to each limit.
Can Claude Code suggest what to do next? Yes. With next-steps, up to 6 suggested next steps appear after every reply; tick one or more and press Send, and it writes a detailed prompt for Claude.
Does it work in the Claude Desktop app? Yes. Both mods work in the Desktop app's Code tab and in the terminal.
Ideas and pull requests are welcome. Each mod is a folder under plugins/; add a new one and list it in .claude-plugin/marketplace.json.
Keywords: Claude Code mod, Claude Code plugin, Claude usage tracker, Claude usage limit, 5-hour limit, weekly limit, rate limit monitor, Claude Desktop, next steps, follow-up suggestions, prompt suggestions, Anthropic Claude.
MIT. See LICENSE.
hooks/register.tsx 259 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Limit } from '../types'
5
6const limits = atom({ plugin: 'usage-band', key: 'limits' } as const, [] as Limit[])
7const now = atom({ plugin: 'usage-band', key: 'now' } as const, 0)
8// True once the engine has measured the session: an account with no limits
9// (an API key) then shows nothing rather than a waiting message.
10const measured = atom({ plugin: 'usage-band', key: 'measured' } as const, false)
11// Whether the session's folder is a git repository: the Push button shows only then.
12const isRepo = atom({ plugin: 'usage-band', key: 'isRepo' } as const, false)
13
14// ---------- push button ----------
15
16// Hands the job to Claude rather than running a bare git push: it commits what is
17// uncommitted with a real message, pushes, and asks before anything risky.
18const PUSH_PROMPT =
19 'Commit all my current changes with a clear commit message and push them to GitHub. ' +
20 'If there is nothing to commit, just push. Tell me in one line what you pushed.'
21
22async function checkRepo($: EngineInterface) {
23 try {
24 const r = await $.process.run(['git', 'rev-parse', '--is-inside-work-tree'])
25 await update($, isRepo, () => r.exitCode === 0 && r.stdout.trim() === 'true')
26 } catch {
27 await update($, isRepo, () => false) // no git installed
28 }
29}
30
31async function pushToGitHub($: EngineInterface) {
32 await $.prompt.submit({ text: PUSH_PROMPT, asUser: true })
33}
34
35// ---------- new chat button ----------
36
37// A mod can't open a Desktop chat itself, so the button presses the app's own
38// shortcut for you: Ctrl+N on Windows (a one-line script that starts in a blink,
39// unlike PowerShell), Cmd+N on macOS. The click just put Claude in front.
40const SEND_CMD_N = 'tell application "System Events" to keystroke "n" using command down'
41
42async function pressNewChat($: EngineInterface) {
43 try {
44 await $.process.run(['wscript.exe', '//B', '//Nologo', `${$.plugin.root}/scripts/new-chat.vbs`])
45 } catch {
46 await $.process.run(['osascript', '-e', SEND_CMD_N]).catch(() => undefined)
47 }
48}
49
50type Window = { label: string; short: string; ms: number }
51const WINDOWS: Record<string, Window> = {
52 five_hour: { label: '5-hour', short: '5H', ms: 5 * 3600_000 },
53 seven_day: { label: 'Weekly', short: 'WK', ms: 7 * 86400_000 },
54 // Team / Enterprise / gateway accounts: no fixed window, so no pace marker
55 spend_limit: { label: 'Spend', short: 'SP', ms: 0 },
56}
57const windowOf = (kind: string): Window =>
58 WINDOWS[kind] ?? { label: kind.replace(/_/g, ' '), short: kind.slice(0, 2).toUpperCase(), ms: 0 }
59
60type Row = {
61 kind: string
62 label: string
63 short: string
64 pct: number
65 /** 0..1 of the window already elapsed, when the reset time is known */
66 elapsed?: number
67 /** where usage lands at reset if the current rate holds */
68 projected?: number
69 resetIn: string
70 /** just the countdown, for the compact layout */
71 resetShort: string
72 level: 'ok' | 'warn' | 'hot'
73}
74
75const fmtDuration = (ms: number) => {
76 const mins = Math.max(0, Math.round(ms / 60000))
77 const d = Math.floor(mins / 1440)
78 const h = Math.floor((mins % 1440) / 60)
79 const m = mins % 60
80 return d ? `${d}d ${h}h` : h ? `${h}h ${m}m` : `${m}m`
81}
82
83const toRow = (l: Limit, at: number): Row => {
84 const w = windowOf(l.kind)
85 const pct = l.percentUsed
86 let elapsed: number | undefined
87 let projected: number | undefined
88 let resetIn = ''
89 let resetShort = ''
90 const resetMs = l.resetsAt ? Date.parse(l.resetsAt) - at : NaN
91 if (resetMs > 0) {
92 resetShort = fmtDuration(resetMs)
93 resetIn = `resets in ${resetShort}`
94 if (w.ms > 0) {
95 elapsed = Math.min(1, Math.max(0, 1 - resetMs / w.ms))
96 if (elapsed > 0.05) projected = Math.round(pct / elapsed)
97 }
98 } else if (l.resetsAt) {
99 resetIn = 'resetting now'
100 resetShort = 'now'
101 }
102 const level =
103 pct >= 90 || (projected ?? 0) >= 100 ? 'hot' : pct >= 70 || (projected ?? 0) >= 85 ? 'warn' : 'ok'
104 return { kind: l.kind, label: w.label, short: w.short, pct, elapsed, projected, resetIn, resetShort, level }
105}
106
107const paceText = (r: Row) =>
108 r.projected === undefined ? '' : r.projected >= 100 ? 'on pace for 100%+' : `on pace for ~${r.projected}%`
109
110// ---------- desktop: SVG card ----------
111
112const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
113
114// Compact: one row per window, stacked. Set in the app's own UI font with
115// tabular figures, so it matches the band's text; 12px everywhere around it.
116const SEP_GAP = 12
117const CONTENT_W = 116
118const SVG_W = SEP_GAP + CONTENT_W + SEP_GAP
119const ROW_H = 17
120const svgHeight = (n: number) => n * ROW_H
121
122const svgCard = (rows: Row[]) => {
123 const PCT_END = 54
124 const cells = rows
125 .map((r, i) => {
126 const base = i * ROW_H + 13
127 return `
128 <text class="label" x="0" y="${base}">${esc(r.short)}</text>
129 <text class="pct ${r.level}" x="${PCT_END}" y="${base}" text-anchor="end">${Math.round(r.pct)}<tspan class="unit">%</tspan></text>
130 <text class="meta" x="${CONTENT_W}" y="${base}" text-anchor="end">${r.resetShort ? '↻ ' + esc(r.resetShort) : ''}</text>`
131 })
132 .join('')
133 const H = svgHeight(rows.length)
134 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SVG_W} ${H}" width="${SVG_W}" height="${H}">
135 <style>
136 svg { --fg:#3d3d3a; --muted:#8a8985; --track:rgba(0,0,0,.12); --warn:#b7791f; --hot:#c4483e;
137 font-family: system-ui, "Segoe UI", -apple-system, sans-serif; font-variant-numeric: tabular-nums; }
138 @media (prefers-color-scheme: dark) {
139 svg { --fg:#d4d3cf; --muted:#8f8e8a; --track:rgba(255,255,255,.14); --warn:#e0a84a; --hot:#e8766c; }
140 }
141 text { fill:var(--muted); }
142 .label { font-size:10px; font-weight:600; letter-spacing:.08em; }
143 .pct { font-size:12.5px; font-weight:600; fill:var(--fg); }
144 .unit { font-size:10px; font-weight:500; }
145 .pct.warn { fill:var(--warn); } .pct.hot { fill:var(--hot); }
146 .meta { font-size:11px; }
147 .sep { stroke:var(--track); stroke-width:1; }
148 </style>
149 <line class="sep" x1="0.5" x2="0.5" y1="1" y2="${H - 1}"/>
150 <line class="sep" x1="${SVG_W - 0.5}" x2="${SVG_W - 0.5}" y1="1" y2="${H - 1}"/>
151 <g transform="translate(${SEP_GAP},0)">${cells}</g>
152</svg>`
153}
154
155// ---------- terminal: plain text ----------
156
157const COLORS = { ok: 'green', warn: 'yellow', hot: 'red' } as const
158
159
160export const register: Register = on => {
161 on('session.start', async ($, e, next) => {
162 const result = await next(e)
163 void checkRepo($)
164 const t = await $.clock.now()
165 await update($, now, () => t)
166 const usage = await $.session.usage()
167 await update($, limits, () => usage.rateLimits.map(l => ({ ...l })))
168 // Tick once a minute so the countdowns and pace marker stay current.
169 $.clock.every(60_000, () => {
170 void $.clock.now().then(t => update($, now, () => t))
171 })
172 return result
173 })
174
175 on('session.measure', async ($, e, next) => {
176 // In the background, so Claude Code never waits on the widget.
177 void update($, measured, () => true)
178 if (e.changed.includes('rateLimits')) void update($, limits, () => e.rateLimits.map(l => ({ ...l })))
179 return next(e)
180 })
181
182 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
183 // Whatever else draws in the band (another mod) stays, on the left.
184 const inner = await next(e)
185 if (e.props.hasSurvey) return inner
186
187 const els = $.ui.resolve(e) as Record<string, any>
188 const { Box, Text, Button } = els
189 const [repo, savedNow, saved, wasMeasured] = await Promise.all([
190 read($, isRepo),
191 read($, now),
192 read($, limits),
193 read($, measured),
194 ])
195 const pushButton = repo ? (
196 <Button key="push" label="↑" variant="primary" onPress={() => pushToGitHub($)} />
197 ) : null
198 const newChatButton = (
199 // The app draws its own buttons; "primary" is its accent (Claude orange) style.
200 <Button key="new-chat" label="+" variant="primary" onPress={() => pressNewChat($)} />
201 )
202 const at = savedNow || (await $.clock.now())
203 const rows = saved.map(l => toRow(l, at))
204
205 if (rows.length === 0) {
206 // No limits on this account (e.g. an API key): stay out of the way.
207 if (wasMeasured) {
208 return (
209 <Box flexDirection="row" alignItems="center" gap={1} flexGrow={1}>
210 <Box flexGrow={1}>{inner}</Box>
211 {pushButton}
212 {newChatButton}
213 </Box>
214 )
215 }
216 return (
217 <Box flexDirection="row" alignItems="center" gap={1} flexGrow={1}>
218 <Box flexGrow={1}>{inner}</Box>
219 <Text dimColor>usage: waiting for first reply</Text>
220 {pushButton}
221 {newChatButton}
222 </Box>
223 )
224 }
225
226 if (e.surface === 'desktop' && els.Svg) {
227 const Svg = els.Svg
228 const alt = rows
229 .map(r => [`${r.label} ${r.pct}% used`, r.resetIn, paceText(r)].filter(Boolean).join(', '))
230 .join('; ')
231 return (
232 <Box flexDirection="row" alignItems="center" gap={1} flexGrow={1}>
233 {/* left side: other mods in the band */}
234 <Box flexGrow={1}>{inner}</Box>
235 <Svg source={svgCard(rows)} alt={alt} width={SVG_W} height={svgHeight(rows.length)} />
236 {pushButton}
237 {newChatButton}
238 </Box>
239 )
240 }
241
242 return (
243 <Box flexDirection="row" gap={3} flexGrow={1}>
244 <Box flexGrow={1}>{inner}</Box>
245 {rows.map(r => (
246 <Box key={r.kind} flexDirection="row">
247 <Text dimColor>{r.short} </Text>
248 <Text color={r.level === 'ok' ? undefined : COLORS[r.level]}>{Math.round(r.pct)}%</Text>
249 <Text dimColor> {r.resetShort}</Text>
250 </Box>
251 ))}
252 <Text dimColor>│</Text>
253 {pushButton}
254 {newChatButton}
255 </Box>
256 )
257 })
258}
259types/index.d.ts 8 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'usage-band': { limits: Limit[]; now: number; measured: boolean; isRepo: boolean }
6 }
7}
8