SLOPSHOPPER

usage-limits

Shows subscription rate-limit usage as bars under the prompt

newspinner
v0.1.0no licenseupdated 2026-10-26seandheath/nixos/packages/claude-usage-limits
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-limits
› 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% · NaNdNaNh

Draws

Prompt hint
⟨Claude Code's own drawing⟩ 5h 31% · NaNdNaNh
Source 2 files
hooks/register.tsx 61 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit } from '../types'
5
6const limits = atom({ plugin: 'usage-limits', key: 'limits' } as const, [] as Limit[])
7
8const BAR_WIDTH = 28
9
10const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
11
12function untilReset(resetsAt: string, now: number): string {
13  const minutes = Math.max(0, Math.round((Date.parse(resetsAt) - now) / 60000))
14  if (minutes < 60) return `${minutes}m`
15  if (minutes < 48 * 60) return `${Math.floor(minutes / 60)}h${minutes % 60}m`
16  return `${Math.floor(minutes / 1440)}d${Math.floor((minutes % 1440) / 60)}h`
17}
18
19export const register: Register = on => {
20  on('session.start', async ($, e, next) => {
21    const { rateLimits } = await $.session.usage()
22    await update($, limits, () => rateLimits)
23    return next(e)
24  })
25
26  on('session.measure', async ($, e, next) => {
27    await update($, limits, () => e.rateLimits)
28    return next(e)
29  })
30
31  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
32    const current = await read($, limits)
33    if (current.length === 0) return next(e)
34
35    const now = await $.clock.now()
36    const { Box, Text } = $.ui.resolve(e)
37
38    return (
39      <Box flexDirection="column">
40        {await next(e)}
41        <Box flexWrap="wrap" columnGap={1}>
42          {current.map(limit => {
43            const reset = limit.resetsAt ? ` · ${untilReset(limit.resetsAt, now)}` : ''
44            const label = `${LABELS[limit.kind] ?? limit.kind} ${limit.percentUsed}%${reset}`
45            const pad = Math.max(0, BAR_WIDTH - label.length)
46            const text = (' '.repeat(Math.floor(pad / 2)) + label).padEnd(BAR_WIDTH)
47            const filled = Math.round((Math.min(limit.percentUsed, 100) / 100) * BAR_WIDTH)
48            const color = limit.percentUsed >= 90 ? 'error' : limit.percentUsed >= 70 ? 'warning' : 'success'
49            return (
50              <Text>
51                <Text color="black" backgroundColor={color}>{text.slice(0, filled)}</Text>
52                <Text color="black" backgroundColor="inactive">{text.slice(filled)}</Text>
53              </Text>
54            )
55          })}
56        </Box>
57      </Box>
58    )
59  })
60}
61
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-limits': { limits: Limit[] }
6  }
7}
8