Shows your Claude 5 hour and weekly rate limits in the status line and warns at 80% and 90%.

A Claude Code mod that shows your Claude 5 hour and weekly rate limits in the status line and sends a toast at 80% and 90%.
/plugin marketplace add 0xGondarxyz/claude-code-mods
/plugin install usage-meter@claude-code-mods
Or try it without installing:
git clone https://github.com/0xGondarxyz/claude-code-mods
claude --plugin-dir claude-code-mods/usage-meter
Mods are not sandboxed. They run with the same access as Claude Code. Read the source before you install any mod, including this one.
Status line:
Claude 5h 62% (resets 18:40) · week 41%
Toast, once when a window first reaches 80% and once more at 90%:
Claude 5h limit at 82%, resets 18:40. Good time to hand big tasks to Sonnet.
If a reading jumps from below 80% straight past 90%, only the 90% toast is sent. It warns again after usage falls back under 80%, which happens when the window resets.
| Window | Label |
|---|---|
| 5 hour | 5h |
| 7 day | week |
| Spend limit (Claude gateway) | spend |
| Anything else | the window's own name |
The reset time is local, 24 hour, and shown on the 5h part only. It is left out when the reset time is missing.
claude plugin test .
MIT
hooks/register.ts 23 lines1import type { Register } from 'claude-code'
2import { dueToasts, localClock, statusText } from './meter'
3
4const fired = new Map<string, number>()
5
6export const register: Register = (on) => {
7 on('session.start', async ($, e, next) => {
8 try {
9 const { rateLimits } = await $.session.usage()
10 if (rateLimits.length > 0) $.ui.status(statusText(rateLimits, localClock))
11 } catch {}
12 return next(e)
13 })
14
15 on('session.measure', ($, e, next) => {
16 try {
17 $.ui.status(statusText(e.rateLimits, localClock))
18 for (const text of dueToasts(e.rateLimits, fired, localClock)) $.ui.toast(text)
19 } catch {}
20 return next(e)
21 })
22}
23hooks/meter.ts 79 lines1export type RateLimit = { kind: string; percentUsed: number; resetsAt?: string }
2
3/** Local clock reading as "HH:MM", or undefined. Injected so tests do not depend on the timezone. */
4export type Clock = (date: Date) => string
5
6export const THRESHOLDS = [80, 90] as const
7
8export const localClock: Clock = (date) =>
9 `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
10
11const ORDER = ['five_hour', 'seven_day']
12
13export function labelFor(kind: string): string {
14 if (kind === 'five_hour') return '5h'
15 if (kind === 'seven_day') return 'week'
16 if (kind === 'spend_limit') return 'spend'
17 return kind
18}
19
20export function sortLimits(limits: RateLimit[]): RateLimit[] {
21 const rank = (kind: string) => {
22 const i = ORDER.indexOf(kind)
23 return i === -1 ? ORDER.length : i
24 }
25 return limits
26 .map((limit, index) => ({ limit, index }))
27 .sort((a, b) => rank(a.limit.kind) - rank(b.limit.kind) || a.index - b.index)
28 .map((x) => x.limit)
29}
30
31export function resetTime(resetsAt: string | undefined, clock: Clock): string | undefined {
32 if (!resetsAt) return undefined
33 const date = new Date(resetsAt)
34 if (Number.isNaN(date.getTime())) return undefined
35 return clock(date)
36}
37
38/** Status line text, or undefined to clear it. */
39export function statusText(limits: RateLimit[], clock: Clock): string | undefined {
40 if (limits.length === 0) return undefined
41 const parts = sortLimits(limits).map((limit) => {
42 let part = `${labelFor(limit.kind)} ${Math.round(limit.percentUsed)}%`
43 if (limit.kind === 'five_hour') {
44 const time = resetTime(limit.resetsAt, clock)
45 if (time) part += ` (resets ${time})`
46 }
47 return part
48 })
49 return `Claude ${parts.join(' · ')}`
50}
51
52export function toastText(limit: RateLimit, clock: Clock): string {
53 const time = resetTime(limit.resetsAt, clock)
54 const resets = time ? `, resets ${time}` : ''
55 return `Claude ${labelFor(limit.kind)} limit at ${Math.round(limit.percentUsed)}%${resets}. Good time to hand big tasks to Sonnet.`
56}
57
58/**
59 * Toasts to send for these readings. `fired` holds, per kind, the highest threshold already
60 * toasted. Dropping under the first threshold clears the entry so the window can warn again.
61 * A reading past several thresholds sends only the highest one.
62 */
63export function dueToasts(limits: RateLimit[], fired: Map<string, number>, clock: Clock): string[] {
64 const out: string[] = []
65 for (const limit of sortLimits(limits)) {
66 const crossed = THRESHOLDS.filter((t) => limit.percentUsed >= t)
67 if (crossed.length === 0) {
68 fired.delete(limit.kind)
69 continue
70 }
71 const highest = crossed[crossed.length - 1] as number
72 if (highest > (fired.get(limit.kind) ?? 0)) {
73 fired.set(limit.kind, highest)
74 out.push(toastText(limit, clock))
75 }
76 }
77 return out
78}
79