Shows the weekly allowance used and when it resets, above the prompt

A Claude Code mod that shows your weekly allowance above the prompt, in the terminal and in the desktop app's Code tab:
Weekly allowance: 42% used · resets Fri Oct 16, 11:00 AM
The reset time is in your computer's local time. The line is dim, with a few parts in bold:
It reads the seven_day rate-limit window that Claude Code reports after each response, so the line appears after your first message in a session. It needs a Claude subscription; with API-key billing there is no weekly window and nothing is shown.
In a terminal, start claude and type:
/plugin install weekly-usage --marketplace manualofideas/claude-weekly-usage
Answer y to add the marketplace, then press Enter to pick the user scope. Installed at the user scope, it also loads in the desktop app's Code tab (the /plugin command itself works only in a terminal).
WARN_AT near the top of hooks/register.tsx.
claude plugin validate .
claude plugin test .hooks/register.tsx 102 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Weekly } from '../types'
5
6const weekly = atom({ plugin: 'weekly-usage', key: 'weekly' } as const, null)
7const warned = atom({ plugin: 'weekly-usage', key: 'warned' } as const, false)
8
9const WARN_AT = 80
10const HOUR = 60 * 60 * 1000
11const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
12const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
13
14// { day: "Thu Oct 16", clock: "9:00 AM" } in the machine's local time.
15const resetParts = (iso: string): { day: string; clock: string } | null => {
16 const d = new Date(iso)
17 if (Number.isNaN(d.getTime())) return null
18 const h = d.getHours()
19 const m = String(d.getMinutes()).padStart(2, '0')
20 return {
21 day: `${DAYS[d.getDay()]} ${MONTHS[d.getMonth()]} ${d.getDate()}`,
22 clock: `${h % 12 === 0 ? 12 : h % 12}:${m} ${h < 12 ? 'AM' : 'PM'}`,
23 }
24}
25
26// "Thu Oct 16, 9:00 AM" in the machine's local time.
27export const formatReset = (iso: string): string => {
28 const p = resetParts(iso)
29 return p ? `${p.day}, ${p.clock}` : iso
30}
31
32export const describe = (w: Weekly): string =>
33 `Weekly allowance: ${w.percentUsed}% used` +
34 (w.resetsAt ? ` · resets ${formatReset(w.resetsAt)}` : '')
35
36type Segment = { text: string; bold?: boolean }
37
38// The line in parts, the ones to stand out marked bold: the percentage at
39// WARN_AT and above; the reset's date and time within 48 hours of it, only
40// its time within 24 hours.
41export const segments = (w: Weekly, now: number): Segment[] => {
42 const out: Segment[] = [
43 { text: 'Weekly allowance: ' },
44 { text: `${w.percentUsed}%`, bold: w.percentUsed >= WARN_AT },
45 { text: ' used' },
46 ]
47 if (!w.resetsAt) return out
48
49 const p = resetParts(w.resetsAt)
50 if (!p) return [...out, { text: ` · resets ${w.resetsAt}` }]
51
52 const left = new Date(w.resetsAt).getTime() - now
53 out.push({ text: ' · resets ' })
54 if (left <= 24 * HOUR) {
55 out.push({ text: `${p.day}, ` }, { text: p.clock, bold: true })
56 } else if (left <= 48 * HOUR) {
57 out.push({ text: `${p.day}, ${p.clock}`, bold: true })
58 } else {
59 out.push({ text: `${p.day}, ${p.clock}` })
60 }
61 return out
62}
63
64export const register: Register = on => {
65 on('session.measure', async ($, e, next) => {
66 const window = e.rateLimits.find(r => r.kind === 'seven_day')
67 if (window) {
68 const w: Weekly = { percentUsed: window.percentUsed, resetsAt: window.resetsAt ?? null }
69 await update($, weekly, () => w)
70
71 // One toast per crossing: re-arms when usage drops back (a new week).
72 if (w.percentUsed < WARN_AT) {
73 await update($, warned, () => false)
74 } else if (!(await read($, warned))) {
75 await update($, warned, () => true)
76 $.ui.toast(`${describe(w)}`)
77 }
78 }
79
80 return next(e)
81 })
82
83 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
84 const w = await read($, weekly)
85 if (e.props.hasSurvey || w === null) {
86 return next(e)
87 }
88
89 const { Box, Text } = $.ui.resolve(e)
90 const now = await $.clock.now()
91
92 // Dim by default; a bold part is drawn in the normal font color.
93 return (
94 <Box>
95 <Text key="weekly">
96 {segments(w, now).map(s => (s.bold ? <Text bold>{s.text}</Text> : <Text dimColor>{s.text}</Text>))}
97 </Text>
98 </Box>
99 )
100 })
101}
102types/index.d.ts 8 lines1export type Weekly = { percentUsed: number; resetsAt: string | null }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'weekly-usage': { weekly: Weekly | null; warned: boolean }
6 }
7}
8