Always shows your 5-hour and weekly plan usage above the prompt, with when each window resets.

A Claude Code mod that always shows your plan usage above the prompt: the 5-hour window, the weekly window, and when each window resets.
5h ███░░░░░░░ 34% · resets 15:30 (in 1h 30m) │ week ██░░░░░░░░ 18% · resets Sun 14:00 (in 3d 0h)
↻ 15:30 and ↻ Sun 14:00; under 70 the bars go too: 5h 34% · ↻ 15:30 │ week 18% · ↻ Sun 14:00.usage: waiting for the first response…, since the numbers arrive with each response.It works in the terminal and in the Claude desktop app's Code tab.
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in the environment Claude Code starts in, unless your account already has them on.Turn mods on. On Windows, as a user environment variable (then fully restart the desktop app or your terminal):
[Environment]::SetEnvironmentVariable("CLAUDE_CODE_ENABLE_FUNCTION_HOOKS", "1", "User")
On macOS or Linux, in your shell profile:
export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1
Then, inside Claude Code:
/plugin marketplace add jumoog/claude_mod_usage
/plugin install usage-meter@usage
/reload-plugins
In the desktop app, if the band does not appear after a restart, run /reload-plugins once more: the Code tab only starts drawing mod UI once it has picked up the plugin.
git clone git@github.com:jumoog/claude_mod_usage.git
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir ./claude_mod_usage/usage-meter
The whole mod is usage-meter/hooks/usage-meter.mjs.
$.session.usage() returns rateLimits, a list of { kind, percentUsed, resetsAt }. The mod keeps five_hour and seven_day.session.start, after every main-agent turn.complete, on session.measure (raised when the rate-limit headers move the numbers), and every 30 seconds via $.clock.every so the countdown stays current.$.state. A ui.render hook on the AbovePrompt component reads it and draws the band, so every save redraws it.claude plugin validate ./usage-meter
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test ./usage-meter
After changing the mod, bump version in usage-meter/.claude-plugin/plugin.json, then:
claude plugin update usage-meter@usage
/reload-plugins
.claude-plugin/marketplace.json marketplace "usage", listing the one plugin
usage-meter/
.claude-plugin/plugin.json plugin manifest
hooks/hooks.json names the hooks module
hooks/usage-meter.mjs the mod
types/index.d.ts $.state contract
tests/usage-meter.test.ts claude plugin test suitehooks/usage-meter.mjs 131 lines1// Usage Meter: your 5-hour and weekly plan usage, always above the prompt,
2// with the time each window resets.
3
4const WIDTH = 10;
5const TICK_MS = 30_000;
6const LABELS = { five_hour: "5h", seven_day: "week" };
7const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
8
9const snapshot = { plugin: "usage-meter", key: "snapshot" };
10
11export function register(on) {
12 on("session.start", async ($, e, next) => {
13 const result = await next(e);
14 await refresh($);
15 // Keep the countdown honest between turns.
16 $.clock.every(TICK_MS, () => refresh($));
17 return result;
18 });
19
20 // Fires whenever Claude Code re-measures the session, including when the
21 // rate-limit headers on a response move the numbers.
22 on("session.measure", async ($, e, next) => {
23 const result = await next(e);
24 if (Array.isArray(e.rateLimits)) {
25 await save($, e.rateLimits);
26 }
27 return result;
28 });
29
30 on("turn.complete", async ($, e, next) => {
31 const result = await next(e);
32 if (!e.agentId) await refresh($);
33 return result;
34 });
35
36 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
37 if (e.props.hasSurvey) return next(e);
38 const { value } = await $.state.get(snapshot);
39 const { Box, Text } = $.ui.resolve(e);
40 return band(Box, Text, value, e.props.bodyColumns);
41 });
42}
43
44async function refresh($) {
45 const { rateLimits } = await $.session.usage();
46 await save($, rateLimits ?? []);
47}
48
49async function save($, rateLimits) {
50 const limits = {};
51 for (const r of rateLimits) {
52 if (r.kind in LABELS) {
53 limits[r.kind] = { percent: clamp(r.percentUsed), resetsAt: r.resetsAt };
54 }
55 }
56 const { value: previous } = await $.state.get(snapshot);
57 // Keep the last known numbers when a measurement comes back without them.
58 const merged = { ...(previous?.limits ?? {}), ...limits };
59 await $.state.set(snapshot, { limits: merged, now: await $.clock.now() });
60}
61
62function band(Box, Text, value, columns) {
63 const limits = value?.limits ?? {};
64 // The desktop app lays the band out itself and may not pass a column count.
65 const roomy = columns === undefined || columns >= 70;
66 // Spelled-out resets with countdowns for both windows need about 105 columns.
67 const verbose = columns === undefined || columns >= 105;
68
69 if (!limits.five_hour && !limits.seven_day) {
70 return Box({
71 paddingX: 1,
72 children: [Text({ dimColor: true, children: "usage: waiting for the first response…" })],
73 });
74 }
75
76 const parts = [];
77 for (const kind of Object.keys(LABELS)) {
78 const limit = limits[kind];
79 if (!limit) continue;
80 if (parts.length) parts.push(Text({ dimColor: true, children: " │ " }));
81 parts.push(...meter(Text, LABELS[kind], limit.percent, roomy));
82 const reset = resetText(limit.resetsAt, value.now, verbose);
83 if (reset) parts.push(Text({ dimColor: true, children: reset }));
84 }
85 return Box({ flexDirection: "row", paddingX: 1, children: parts });
86}
87
88function meter(Text, label, percent, roomy) {
89 const color = percent >= 80 ? "red" : percent >= 50 ? "yellow" : "green";
90 const parts = [Text({ bold: true, children: `${label} ` })];
91 if (roomy) {
92 const filled = Math.round((percent / 100) * WIDTH);
93 if (filled > 0) parts.push(Text({ color, children: "█".repeat(filled) }));
94 parts.push(Text({ dimColor: true, children: "░".repeat(WIDTH - filled) + " " }));
95 }
96 parts.push(Text({ color, bold: true, children: `${Math.round(percent)}%` }));
97 return parts;
98}
99
100function resetText(resetsAt, now, verbose) {
101 const at = Date.parse(resetsAt);
102 if (!Number.isFinite(at)) return "";
103 const left = at - now;
104 if (left <= 0) return " · resetting…";
105 const clock = when(new Date(at), new Date(now));
106 return verbose ? ` · resets ${clock} (in ${duration(left)})` : ` · ↻ ${clock}`;
107}
108
109// A bare time for today; anything later also gets the weekday.
110function when(d, today) {
111 const sameDay = d.toDateString() === today.toDateString();
112 return sameDay ? hhmm(d) : `${DAYS[d.getDay()]} ${hhmm(d)}`;
113}
114
115function hhmm(d) {
116 return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
117}
118
119function duration(ms) {
120 const mins = Math.max(1, Math.ceil(ms / 60_000));
121 const days = Math.floor(mins / 1440);
122 const h = Math.floor(mins / 60) % 24;
123 const m = mins % 60;
124 if (days) return `${days}d ${h}h`;
125 return h ? `${h}h ${m}m` : `${m}m`;
126}
127
128function clamp(n) {
129 return Math.min(100, Math.max(0, Number(n) || 0));
130}
131types/index.d.ts 13 lines1export type UsageMeterLimit = { percent: number; resetsAt: string };
2
3export type UsageMeterSnapshot = {
4 limits: { five_hour?: UsageMeterLimit; seven_day?: UsageMeterLimit };
5 now: number;
6};
7
8declare module "claude-code" {
9 interface PluginState {
10 "usage-meter": { snapshot: UsageMeterSnapshot };
11 }
12}
13