Show 5-hour/7-day usage limits, time until reset, context, and session cost in the status line

A Claude Code mod that keeps your usage in the status line: 5-hour and 7-day limits with time until reset, context fill, and session cost. Works in the Claude desktop app's Code tab and in the terminal.

15% used 2h 48m · 8% used 6d 9h · 187k/1M 19% · $3.53
| Item | Shows |
|---|---|
| 5-hour limit | Usage and time until reset |
| 7-day limit | Usage and time until reset |
| Context | Tokens used / window size and percent |
| Session cost | Tokens used so far, priced at API rates |
Run claude in a terminal, then
/plugin install usage-status --marketplace sijunnoh/claude-usage-status
Install at user scope and it turns on in every new session. The command isn't available in the desktop app's Code tab, but once installed from a terminal, the mod shows up in the desktop app too.
claude plugin validate .
claude plugin test .
Built against Claude Code 2.1.295. The mod API is new and may change.
Claude mod가 나와서 만든 첫 플러그인, usage-status
MIT
hooks/register.ts 152 lines1import type { EngineInterface, Register, SessionContextUsage, SessionCost, SessionRateLimit, Timer } from 'claude-code'
2
3type Figures = {
4 context: SessionContextUsage
5 rateLimits: SessionRateLimit[]
6 cost?: SessionCost
7}
8
9// Order: 5-hour → 7-day → others
10const ORDER = ['five_hour', 'seven_day']
11const LABEL: Record<string, string> = { five_hour: '5-hour', seven_day: '7-day' }
12const rank = (kind: string) => {
13 const i = ORDER.indexOf(kind)
14 return i === -1 ? ORDER.length : i
15}
16
17// Separator — plain spaces can collapse when rendered, so use em spaces
18export const SEP = '\u2003·\u2003'
19
20// Tokens: 999 · 84k · 1M · 1.5M
21export const fmtTokens = (n: number) => {
22 if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`
23 if (n >= 1000) return `${Math.round(n / 1000)}k`
24 return `${n}`
25}
26
27// Time left: 6d 9h · 3h 3m · 42m
28export const fmtLeft = (ms: number) => {
29 const min = Math.max(0, Math.floor(ms / 60000))
30 const d = Math.floor(min / 1440)
31 const h = Math.floor((min % 1440) / 60)
32 const m = min % 60
33 if (d > 0) return `${d}d ${h}h`
34 if (h > 0) return `${h}h ${m}m`
35 return `${m}m`
36}
37
38const fmtLimit = (r: SessionRateLimit, now: number) => {
39 const used = `${Math.round(r.percentUsed)}% used`
40 if (!ORDER.includes(r.kind)) return `${used} ${r.kind}`
41 if (!r.resetsAt) return used
42 return `${used} ${fmtLeft(Date.parse(r.resetsAt) - now)}`
43}
44
45export const format = ({ context, rateLimits, cost }: Figures, now: number) => {
46 const parts = [...rateLimits]
47 .sort((a, b) => rank(a.kind) - rank(b.kind))
48 .map(r => fmtLimit(r, now))
49
50 if (context.tokens !== undefined) {
51 // Compute it when the engine gives no percent
52 const percent = context.percent ?? (context.tokens / context.window) * 100
53 const pct = ` ${Math.round(percent)}%`
54 parts.push(`${fmtTokens(context.tokens)}/${fmtTokens(context.window)}${pct}`)
55 }
56 if (cost) parts.push(`$${cost.usd.toFixed(2)}`)
57
58 return parts.length ? parts.join(SEP) : undefined
59}
60
61// Warn once when usage crosses these
62const THRESHOLDS = [80, 95]
63
64// Newly crossed thresholds — same kind + reset time means the same window
65export const crossed = (rateLimits: SessionRateLimit[], warned: Set<string>) =>
66 rateLimits.flatMap(r =>
67 THRESHOLDS.filter(t => r.percentUsed >= t)
68 .map(t => ({ key: `${r.kind}:${r.resetsAt ?? ''}:${t}`, kind: r.kind, threshold: t }))
69 .filter(c => !warned.has(c.key)),
70 )
71
72// Drop windows whose reset time has passed
73export const fresh = (limits: SessionRateLimit[], now: number) =>
74 limits.filter(r => !r.resetsAt || Date.parse(r.resetsAt) > now)
75
76// Some responses carry no rate-limit headers; keep the last known window of
77// each kind until it resets, and let newer readings replace it
78export const mergeLimits = (known: SessionRateLimit[], next: SessionRateLimit[], now: number) => {
79 const kinds = new Set(next.map(r => r.kind))
80 return fresh([...next, ...known.filter(r => !kinds.has(r.kind))], now)
81}
82
83const STORE_KEY = 'rateLimits'
84
85const isLimit = (v: unknown): v is SessionRateLimit =>
86 typeof v === 'object' && v !== null &&
87 typeof (v as SessionRateLimit).kind === 'string' &&
88 typeof (v as SessionRateLimit).percentUsed === 'number'
89
90// Write the windows to the store only when they changed; returns what is stored now
91async function persist($: EngineInterface, limits: SessionRateLimit[], saved: string) {
92 const json = JSON.stringify(limits)
93 if (json !== saved) await $.store.set(STORE_KEY, limits)
94 return json
95}
96
97function warn($: EngineInterface, rateLimits: SessionRateLimit[], warned: Set<string>) {
98 for (const c of crossed(rateLimits, warned)) {
99 warned.add(c.key)
100 $.ui.toast(`${LABEL[c.kind] ?? c.kind} limit passed ${c.threshold}%`)
101 }
102}
103
104export const register: Register = on => {
105 let last: Figures | undefined
106 // Last known limit windows, also kept in $.store so a new session starts with them
107 let known: SessionRateLimit[] = []
108 let saved = ''
109 const warned = new Set<string>()
110 // Keep a single timer even if session.start fires again
111 let timer: Timer | undefined
112
113 on('session.start', async ($, e, next) => {
114 const result = await next(e)
115 const show = async () => {
116 if (!last) return
117 const now = await $.clock.now()
118 $.ui.status(format({ ...last, rateLimits: fresh(last.rateLimits, now) }, now))
119 }
120
121 const stored = await $.store.get(STORE_KEY)
122 known = Array.isArray(stored) ? stored.filter(isLimit) : []
123 saved = JSON.stringify(known)
124
125 const usage = await $.session.usage()
126 known = mergeLimits(known, usage.rateLimits, await $.clock.now())
127 saved = await persist($, known, saved)
128 last = { ...usage, rateLimits: known }
129 await show()
130 warn($, known, warned)
131
132 // Time left is in minutes — redraw at the next minute boundary, then every minute
133 const now = await $.clock.now()
134 timer?.cancel()
135 timer = $.clock.after(60_000 - (now % 60_000), () => {
136 void show()
137 timer = $.clock.every(60_000, () => void show())
138 })
139 return result
140 })
141
142 on('session.measure', async ($, e, next) => {
143 const now = await $.clock.now()
144 known = mergeLimits(known, e.rateLimits, now)
145 saved = await persist($, known, saved)
146 last = { ...e, rateLimits: known }
147 $.ui.status(format(last, now))
148 warn($, known, warned)
149 return next(e)
150 })
151}
152