Your subscription usage at the bottom right of Claude Code: the 5-hour and weekly windows, as a bar that takes turns.

A Claude Code mod: your subscription usage, always at the bottom right, on the same line as the permission mode.
⏵⏵ auto mode on 5h ━━━━━━──── 58% · 2h 10m
Every 8 seconds it switches between the 5-hour window and the weekly one. The bar turns yellow from 75% and red from 90%. After it the time until that window resets.
There is no monthly window: Claude Code only reports the 5-hour and the weekly ones. Off a subscription (API key billing) there is nothing to show, so the footer stays as it is.
While it is on, it also drops the (shift+tab to cycle) hint after the permission mode, to leave room for the meter. Turn that off in /config (hideCycleHint).
/plugin marketplace add midudev/better-claude
/plugin install sub-meter@better-claude
One hooks module (hooks/register.tsx):
session.measure hands it the rate-limit windows after each response (five_hour, seven_day); $.session.usage() gives the first reading at start.$.store, so a new session shows it before its first response.ui.render hook on SessionMode wraps what the engine draws there (its mode labels) and adds the meter after it; $.clock.every switches the window.ui.render hook on PromptHint rewrites the hint line without (shift+tab to cycle); the engine still draws it, and the mode label before it.claude plugin validate ./sub-meter # from the repository root
claude plugin test ./sub-meter # runs hooks/register.test.tsx against the engine
MIT © Miguel Ángel Durán (midudev)
hooks/register.tsx 102 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Limit } from '../types'
5
6const limits = atom({ plugin: 'sub-meter', key: 'limits' } as const, [] as Limit[])
7const view = atom({ plugin: 'sub-meter', key: 'view' } as const, 0)
8
9const WINDOWS = ['five_hour', 'seven_day']
10const LABELS: Record<string, string> = { five_hour: '5h', seven_day: 'week' }
11const ROTATE_MS = 8000
12const CELLS = 10
13const CYCLE_HINT = /\s*\([^()]+ to cycle\)/
14
15/** The subscription windows the meter shows, in order, from what the engine measured. */
16const windowsOf = (rateLimits: readonly SessionRateLimit[]): Limit[] =>
17 WINDOWS.flatMap(kind => {
18 const found = rateLimits.find(limit => limit.kind === kind)
19 return found ? [{ kind, percentUsed: found.percentUsed, resetsAt: found.resetsAt }] : []
20 })
21
22const colorFor = (percent: number) => (percent >= 90 ? 'error' : percent >= 75 ? 'warning' : 'claude')
23
24/** "42m", "3h 5m", "2d 4h": how long until the window resets. */
25function until (ms: number) {
26 const minutes = Math.max(1, Math.round(ms / 60000))
27 if (minutes < 60) return `${minutes}m`
28 const hours = Math.floor(minutes / 60)
29 if (hours < 24) return `${hours}h ${minutes % 60}m`
30 return `${Math.floor(hours / 24)}d ${hours % 24}h`
31}
32
33/** Keeps the latest reading for the meter, and for the next session until a response reports again. */
34async function remember ($: EngineInterface, found: Limit[]) {
35 if (found.length === 0) return
36 await update($, limits, () => found)
37 await $.store.set('limits', found)
38}
39
40export const register: Register = (on, options) => {
41 const hideCycleHint = options.hideCycleHint !== false
42
43 on('session.start', async ($, e, next) => {
44 const saved = await $.store.get('limits')
45 if (Array.isArray(saved)) await update($, limits, () => saved as Limit[])
46
47 const usage = await $.session.usage()
48 await remember($, windowsOf(usage.rateLimits))
49
50 $.clock.every(ROTATE_MS, () => { void update($, view, n => n + 1) })
51 return next(e)
52 })
53
54 on('session.measure', async ($, e, next) => {
55 await remember($, windowsOf(e.rateLimits))
56 return next(e)
57 })
58
59 // Bottom right of the prompt footer: the engine's mode labels, then the meter.
60 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
61 const all = await read($, limits)
62 if (all.length === 0) return next(e)
63
64 const now = await $.clock.now()
65 const shown = all[(await read($, view)) % all.length]
66 const resetsIn = shown.resetsAt === undefined ? undefined : Date.parse(shown.resetsAt) - now
67 const hasReset = resetsIn !== undefined && resetsIn <= 0
68 const percent = hasReset ? 0 : shown.percentUsed
69 const filled = Math.round((Math.min(Math.max(percent, 0), 100) / 100) * CELLS)
70 // The engine's own labels (and any other plugin's) stay as drawn, before the meter.
71 const labels = e.props.modes.length > 0 ? await next(e) : null
72 const { Box, Text } = $.ui.resolve(e)
73
74 return (
75 <Box flexDirection="row">
76 {labels}
77 {labels ? <Text dimColor> · </Text> : null}
78 <Text dimColor>{LABELS[shown.kind] ?? shown.kind} </Text>
79 <Text color={colorFor(percent)}>{'━'.repeat(filled)}</Text>
80 <Text dimColor>{'─'.repeat(CELLS - filled)}</Text>
81 <Text> {Math.round(percent)}%</Text>
82 {resetsIn !== undefined && !hasReset ? <Text dimColor> · {until(resetsIn)}</Text> : null}
83 </Box>
84 )
85 })
86
87 // The engine draws the mode label itself; the hint line after it starts with "(shift+tab to cycle)".
88 if (hideCycleHint) {
89 on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
90 if (!CYCLE_HINT.test(e.props.hint)) return next(e)
91
92 const hint = e.props.hint
93 .replace(CYCLE_HINT, ' · ')
94 .split('·')
95 .map(part => part.trim())
96 .filter(Boolean)
97 .join(' · ')
98 return next({ ...e, props: { ...e.props, hint } })
99 })
100 }
101}
102types/index.d.ts 9 lines1/** One rate-limit window of the subscription, as the last API response reported it. */
2export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'sub-meter': { limits: Limit[]; view: number }
7 }
8}
9