SLOPSHOPPER

usage-hud

A line under the prompt with 5h / 7d usage limits and context window as colored bars

newspinnertimer
v0.1.1no licenseupdated 2026-10-15qiuyue97/claude-usage-hud
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-hud
› 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% ↻ NaNm 7d ▱▱▱▱▱▱▱▱▱▱▱▱ – context ▰▰▰▰▰▰▱▱▱▱▱▱ 49% 97.4k/200k

Draws

Prompt hint
⟨Claude Code's own drawing⟩ 5h ▰▰▰▰▱▱▱▱▱▱▱▱ 31% ↻ NaNm 7d ▱▱▱▱▱▱▱▱▱▱▱▱ – context ▰▰▰▰▰▰▱▱▱▱▱▱ 49% 97.4k/200k
README

claude-usage-hud

A Claude Code mod that shows your 5-hour and 7-day usage limits and the context window as colored bars, right under the prompt input.

? for shortcuts
5h ▰▰▰▰▱▱▱▱▱▱▱▱  34%  ↻ 1h12m   7d ▰▱▱▱▱▱▱▱▱▱▱▱  11%  ↻ 1d5h   context ▰▱▱▱▱▱▱▱▱▱▱▱   6%  59.3k/1M
  • Bars turn green / yellow / red at 50% and 80%.
  • ↻ is the time until the window resets; refreshes every minute.
  • ▲ fast appears when you are using a window faster than its time is passing.
  • Narrow terminals (< 100 columns) stack the three items vertically.
  • 5h / 7d need a Claude Pro or Max subscription and show – until the first reply of a session.

Install

At the prompt of a Claude Code terminal session:

/plugin install usage-hud --marketplace qiuyue97/claude-usage-hud

Answer y to add the marketplace, then pick the user scope so every session loads it.

From a shell:

claude plugin install usage-hud --marketplace qiuyue97/claude-usage-hud

Update later with claude plugin update usage-hud.

Source 2 files
hooks/register.tsx 171 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, SessionUsage, SessionMeasureInput } from 'claude-code'
3
4import type { Limit, Snapshot } from '../types'
5
6const snapshot = atom({ plugin: 'usage-hud', key: 'snapshot' } as const, null)
7const now = atom({ plugin: 'usage-hud', key: 'now' } as const, 0)
8
9const WINDOW_MS: Record<string, number> = {
10  five_hour: 5 * 3600_000,
11  seven_day: 7 * 86400_000,
12}
13
14const LABEL: Record<string, string> = {
15  five_hour: '5h',
16  seven_day: '7d',
17  spend_limit: '$',
18}
19
20const toSnapshot = (u: SessionUsage | SessionMeasureInput): Snapshot => ({
21  context: {
22    tokens: u.context.tokens,
23    window: u.context.window,
24    percent: u.context.percent,
25  },
26  limits: u.rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt })),
27})
28
29export const tone = (pct: number) => (pct < 50 ? 'success' : pct < 80 ? 'warning' : 'error')
30
31export const tokens = (n: number) =>
32  n >= 1_000_000
33    ? `${+(n / 1_000_000).toFixed(1)}M`
34    : n >= 1000
35      ? `${+(n / 1000).toFixed(1)}k`
36      : `${n}`
37
38export const countdown = (ms: number) => {
39  const m = Math.max(0, Math.round(ms / 60_000))
40  const d = Math.floor(m / 1440)
41  const h = Math.floor((m % 1440) / 60)
42  const mm = m % 60
43
44  if (d > 0) return `${d}d${h}h`
45  if (h > 0) return `${h}h${String(mm).padStart(2, '0')}m`
46
47  return `${mm}m`
48}
49
50/** Fraction of the window already elapsed, or undefined when unknown. */
51const pace = (limit: Limit, at: number) => {
52  const span = WINDOW_MS[limit.kind]
53  if (!span || !limit.resetsAt || !at) return undefined
54  const left = Date.parse(limit.resetsAt) - at
55
56  return Math.min(1, Math.max(0, 1 - left / span))
57}
58
59export const register: Register = on => {
60  on('session.start', async ($, e, next) => {
61    const result = await next(e)
62    const usage = await $.session.usage()
63    await update($, snapshot, () => toSnapshot(usage))
64    const at = await $.clock.now()
65    await update($, now, () => at)
66    // keep reset countdowns fresh between turns, and pick up whatever the engine last heard
67    $.clock.every(60_000, async () => {
68      const [t, u] = await Promise.all([$.clock.now(), $.session.usage()])
69      await update($, snapshot, () => toSnapshot(u))
70      await update($, now, () => t)
71    })
72
73    return result
74  })
75
76  on('session.measure', async ($, e, next) => {
77    await update($, snapshot, () => toSnapshot(e))
78    const at = await $.clock.now()
79    await update($, now, () => at)
80
81    return next(e)
82  })
83
84  // drawn under the prompt: the engine's hint line, then the HUD beneath it
85  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
86    const hint = await next(e)
87
88    // drawing is pure: before the first measurement, read the figures directly
89    const snap = (await read($, snapshot)) ?? toSnapshot(await $.session.usage())
90    let at = await read($, now)
91    if (!at) at = await $.clock.now()
92
93    const { Box, Text } = $.ui.resolve(e)
94    const isWide = (e.viewport?.columns ?? 80) >= 100
95    const width = isWide ? 12 : 10
96
97    const bar = (pct: number) => {
98      const p = Math.min(100, Math.max(0, pct))
99      // at least one filled cell once anything is used, so 1–8% still shows
100      const filled = p === 0 ? 0 : Math.max(1, Math.round((p / 100) * width))
101
102      return (
103        <Text>
104          <Text color={tone(pct)}>{'▰'.repeat(filled)}</Text>
105          <Text color="inactive" dimColor>
106            {'▱'.repeat(width - filled)}
107          </Text>
108        </Text>
109      )
110    }
111
112    const metric = (key: string, label: string, pct: number | undefined, tail: string, elapsed?: number) => (
113      <Box key={key} flexDirection="row" gap={1}>
114        <Text color="claude" weight="bold">
115          {label.padEnd(3)}
116        </Text>
117        {pct === undefined ? (
118          <Text color="inactive" dimColor>
119            {'▱'.repeat(width)}
120          </Text>
121        ) : (
122          bar(pct)
123        )}
124        <Text color={pct === undefined ? 'inactive' : tone(pct)} weight="bold">
125          {pct === undefined ? '  –' : `${String(Math.round(pct)).padStart(3)}%`}
126        </Text>
127        {tail ? <Text dimColor>{tail}</Text> : null}
128        {/* used faster than the window's time is passing */}
129        {pct !== undefined && elapsed !== undefined && pct > elapsed * 100 + 5 ? (
130          <Text color="warning">▲ fast</Text>
131        ) : null}
132      </Box>
133    )
134
135    const ctx = snap.context
136    const ctxTail = ctx.tokens === undefined ? `of ${tokens(ctx.window)}` : `${tokens(ctx.tokens)}/${tokens(ctx.window)}`
137    const rows = []
138
139    for (const kind of ['five_hour', 'seven_day']) {
140      const limit = snap.limits.find(l => l.kind === kind)
141      // past its reset time the window has rolled over; new figures arrive with the next reply
142      const expired = !!limit?.resetsAt && Date.parse(limit.resetsAt) <= at
143      const reset = !limit?.resetsAt ? '' : expired ? '↻ reset' : `↻ ${countdown(Date.parse(limit.resetsAt) - at)}`
144      rows.push(
145        metric(
146          kind,
147          LABEL[kind] ?? kind,
148          expired ? 0 : limit?.percentUsed,
149          reset,
150          expired ? undefined : limit && pace(limit, at),
151        ),
152      )
153    }
154
155    for (const limit of snap.limits.filter(l => !WINDOW_MS[l.kind])) {
156      rows.push(metric(limit.kind, LABEL[limit.kind] ?? limit.kind, limit.percentUsed, ''))
157    }
158
159    rows.push(metric('context', 'context', ctx.percent, ctxTail))
160
161    return (
162      <Box flexDirection="column">
163        {hint}
164        <Box flexDirection={isWide ? 'row' : 'column'} columnGap={3}>
165          {rows}
166        </Box>
167      </Box>
168    )
169  })
170}
171
types/index.d.ts 13 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Snapshot = {
4  context: { tokens?: number; window: number; percent?: number }
5  limits: Limit[]
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'usage-hud': { snapshot: Snapshot | null; now: number }
11  }
12}
13