Band above the prompt showing context window fill, rate-limit windows, and session cost.

A Claude Code mod that keeps your context window, usage limits, and session cost visible in a row above the prompt, so you don't have to open the usage popup to check them.

| Section | Means |
|---|---|
ctx | How full the context window is, then tokens used out of the window size |
5h | How much of your 5-hour limit you've used, then when it resets |
7d | How much of your weekly limit you've used, then when it resets |
$ | What this session has cost so far |
These are the same figures the status line and the usage popup show.
Run these in Claude Code:
/plugin marketplace add dblanken-yale/usage-meter
/plugin install usage-meter@usage-meter
Or from a terminal:
claude plugin marketplace add dblanken-yale/usage-meter
claude plugin install usage-meter@usage-meter
Start a new Claude Code session, in the terminal or the desktop app.
Turn on auto-update for the usage-meter marketplace in /plugin (Marketplaces tab), and new versions install when Claude Code starts. To update by hand:
claude plugin marketplace update usage-meter
Desktop app sessions pick up the new version when you start a new one.
Clone the repo and load it from the folder, so edits reload as you save:
git clone git@github.com:dblanken-yale/usage-meter.git ~/code/usage-meter
claude --plugin-dir ~/code/usage-meter
Disable the marketplace install while you do this, or it loads twice.
Bump version in .claude-plugin/plugin.json with every release. Installed copies only update when the version changes.
next(e) and stacks whatever the mods beneath drew under its own row. A mod above it that returns only its own row hides it; cache-buster stacks the same way, so the two show together. In the desktop app there's a little space between the rows; the terminal gets none, since one step of padding there is a whole blank line.claude plugin validate ~/code/usage-meter and claude plugin test ~/code/usage-meter.tsconfig.json points at .claude-plugin/types/, which Claude Code generates and git ignores, so type-checking a fresh clone needs those files regenerated first.hooks/register.tsx 84 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit, Snapshot } from '../types'
5
6const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
7
8const usage = atom({ plugin: 'usage-meter', key: 'usage' } as const, null)
9const now = atom({ plugin: 'usage-meter', key: 'now' } as const, 0)
10
11const colorFor = (pct: number) => (pct >= 90 ? 'red' : pct >= 70 ? 'yellow' : 'green')
12
13const timeFmt = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' })
14const dayTimeFmt = new Intl.DateTimeFormat(undefined, { weekday: 'short', hour: 'numeric', minute: '2-digit' })
15
16// Clock time of the reset; weekday only when it is not within the next ~day.
17function resetAt(limit: Limit, t: number) {
18 if (!limit.resetsAt) return ''
19 const at = Date.parse(limit.resetsAt)
20 return (at - t > 20 * 3_600_000 ? dayTimeFmt : timeFmt).format(at)
21}
22
23export const register: Register = on => {
24 on('session.start', async ($, e, next) => {
25 const started = await next(e)
26 const [{ context, rateLimits, cost }, t0] = await Promise.all([$.session.usage(), $.clock.now()])
27 const snap: Snapshot = { context, rateLimits, cost }
28 await update($, usage, () => snap)
29 await update($, now, () => t0)
30 $.clock.every(60_000, async () => {
31 const t = await $.clock.now()
32 await update($, now, () => t)
33 })
34 return started
35 })
36
37 on('session.measure', async ($, e, next) => {
38 const snap: Snapshot = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
39 await update($, usage, () => snap)
40 return next(e)
41 })
42
43 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
44 // Stack on whatever the next plugin (e.g. cache-buster) draws instead of hiding it.
45 const below = await next(e)
46 const u = await read($, usage)
47 if (e.props.hasSurvey || u === null) return below
48
49 const t = await read($, now)
50 const { Box, Text } = $.ui.resolve(e)
51
52 const meter = (key: string, label: string, pct: number, suffix = '') => {
53 const color = colorFor(pct)
54 return (
55 <Box key={key}>
56 <Text bold>{label} </Text>
57 <Text color={color}>{Math.round(pct)}%</Text>
58 {suffix ? <Text dimColor> {suffix}</Text> : null}
59 </Box>
60 )
61 }
62
63 const ctxPct = u.context.percent ?? 0
64 const ctxK = u.context.tokens ? `${Math.round(u.context.tokens / 1000)}k/${Math.round(u.context.window / 1000)}k` : ''
65
66 const sections = [
67 meter('ctx', 'ctx', ctxPct, ctxK),
68 ...u.rateLimits.map(l => meter(l.kind, LABELS[l.kind] ?? l.kind, l.percentUsed, resetAt(l, t))),
69 ...(u.cost ? [<Text key="cost" bold>${u.cost.usd.toFixed(2)}</Text>] : []),
70 ]
71
72 return (
73 // A little space under the band above (cache-buster); on the terminal a step is a whole blank row, so skip it there.
74 <Box flexDirection="column" paddingTop={e.surface === 'terminal' ? 0 : 1}>
75 <Box>
76 <Text color={colorFor(ctxPct)}>● </Text>
77 {sections.flatMap((section, i) => (i ? [<Text key={`sep${i}`} dimColor>{' │ '}</Text>, section] : [section]))}
78 </Box>
79 {below}
80 </Box>
81 )
82 })
83}
84types/index.d.ts 18 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Snapshot = {
3 context: { tokens?: number; window: number; percent?: number }
4 rateLimits: Limit[]
5 cost?: { usd: number }
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'usage-meter': {
11 /** Latest usage reading; null before the first. */
12 usage: Snapshot | null
13 /** Epoch ms, refreshed every minute so the weekday rule stays current. */
14 now: number
15 }
16 }
17}
18