SLOPSHOPPER

Usage Band: 5-hour & weekly limits

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…

newbandprocesstimer
★ 1v1.5.1MITupdated 2026-10-03pawandeepdhall/claude-mods/plugins/usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ⟨Claude Code's own drawing⟩ 5H 31% now │ [ + ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ 5H 31% now │ [ + ]
README

Claude Code Mods: Usage Limits Tracker & One-Click Next Steps

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 of usage-band and next-steps, with voice-over

▶ Watch the 37-second demo (with voice-over)

Claude Code usage limits mod: 5-hour and weekly usage percentage with reset countdown above the prompt

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

usage-band: always-visible Claude usage limits

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:

  • 5-hour limit: percentage used and a countdown to when it resets (↻ 2h 55m)
  • Weekly limit: percentage used and the days and hours until it resets (↻ 4d 0h)
  • Early warning: the percentage turns amber when you are close, and red when you are near the limit or on pace to hit it before it resets
  • + New chat button: opens a brand-new chat in one click (presses Ctrl+N / Cmd+N for you)
  • ↑ Push button (git projects only): asks Claude to commit your changes and push them to GitHub

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.

next-steps: one-click next-step suggestions in Claude Code

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.

  • Tick one or more, then press Send. The ticked steps go to Claude as one message.
  • Send writes a proper prompt, not a one-liner: it names the files, commands and values from your conversation, adds the context Claude needs and says what done looks like. Several steps become a numbered plan.
  • Labels are short (30 characters at most, so the band never overflows) and specific to what you just asked and what Claude answered
  • They hide while Claude is working and refresh after each reply; typing your own message clears them
  • Fast: Haiku writes the labels and the Send prompt (set 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.json

Requirements

Claude 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

Install

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.

Update

claude plugin marketplace update my-claude-mods
claude plugin update usage-band@my-claude-mods
claude plugin update next-steps@my-claude-mods

What these mods can access

Mods run with your permissions, so here is exactly what each one does. Check it yourself with claude plugin validate ./plugins/<mod>.

  • usage-band reads your session's usage figures ($.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.
  • next-steps sends your last request and Claude's last answer (trimmed) through your own Claude Code session ($.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.

FAQ

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.

Contributing

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.

License

MIT. See LICENSE.

Source 2 files
hooks/register.tsx 259 lines
1import { 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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}
259
types/index.d.ts 8 lines
1export 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