Shows remaining rate-limit (5h / 7d) as colored bars above the prompt

Shows remaining Claude Code rate limits above the prompt as colored, 10-cell bars with reset countdowns.
Requires Claude Code with function-hook plugin support. Validated and tested with Claude Code 2.1.294.
Run in Claude Code:
/plugin install limits-status --marketplace mlopian/claude-plugins
For installation from a local clone, see the repository README. Start a new Claude Code session after installing.
5h, the seven-day window as 7d, and the spend limit as $. Other limit kinds retain their names.claude --version and whether claude plugin install --help lists --marketplace. Use a Claude Code release with function-hook plugin support./plugin to confirm limits-status is installed and enabled, then start a new session.Run from the repository root:
claude plugin validate limits-status
claude plugin test limits-status
Tests cover formatting, color thresholds, countdowns and rendering in terminal and desktop surfaces.
hooks/register.tsx 78 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, ThemeKey } from 'claude-code'
3
4import type { Limit } from '../types'
5
6const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: '$' }
7const BAR_WIDTH = 10
8const MINUTE = 60_000
9
10const snapshot = atom({ plugin: 'limits-status', key: 'snapshot' } as const, { limits: [], now: 0 })
11
12export const bar = (percentLeft: number) => {
13 const filled = Math.round((Math.max(0, Math.min(100, percentLeft)) / 100) * BAR_WIDTH)
14 return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
15}
16
17export const countdown = (ms: number) => {
18 const minutes = Math.max(0, Math.ceil(ms / MINUTE))
19 const d = Math.floor(minutes / 1440)
20 const h = Math.floor((minutes % 1440) / 60)
21 const m = minutes % 60
22 return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`
23}
24
25export const colorFor = (percentLeft: number): ThemeKey =>
26 percentLeft > 50 ? 'success' : percentLeft > 20 ? 'warning' : 'error'
27
28export const segment = (l: Limit, now: number) => {
29 const left = Math.max(0, Math.round((100 - l.percentUsed) * 10) / 10)
30 return {
31 label: LABELS[l.kind] ?? l.kind,
32 bar: bar(left),
33 color: colorFor(left),
34 text: `${left}%${l.resetsAt ? ` reset ${countdown(Date.parse(l.resetsAt) - now)}` : ''}`,
35 }
36}
37
38async function refresh($: EngineInterface, limits?: Limit[]) {
39 const now = await $.clock.now()
40 await update($, snapshot, s => ({ limits: limits ?? s.limits, now }))
41}
42
43export const register: Register = on => {
44 on('session.start', async ($, e, next) => {
45 await refresh($, (await $.session.usage()).rateLimits)
46 $.clock.every(MINUTE, () => refresh($))
47 return next(e)
48 })
49
50 on('session.measure', async ($, e, next) => {
51 if (e.changed.includes('rateLimits')) await refresh($, e.rateLimits)
52 return next(e)
53 })
54
55 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
56 const { limits, now } = await read($, snapshot)
57 if (e.props.hasSurvey || limits.length === 0) return next(e)
58
59 const { Box, Text } = $.ui.resolve(e)
60
61 return (
62 <Box>
63 {limits.map((l, i) => {
64 const s = segment(l, now)
65 return (
66 <Text key={l.kind}>
67 {i > 0 ? ' ' : ''}
68 <Text dimColor>{s.label} </Text>
69 <Text color={s.color}>{s.bar}</Text>
70 <Text dimColor> {s.text}</Text>
71 </Text>
72 )
73 })}
74 </Box>
75 )
76 })
77}
78types/index.d.ts 9 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Snapshot = { limits: Limit[]; now: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'limits-status': { snapshot: Snapshot }
7 }
8}
9