Status line with the model, context fill, session tokens and cost, and the 5-hour and weekly usage limits.

A Claude Code mod that puts your usage in the status line: the model and its effort, the context fill, the session's tokens and cost, and how much of your 5-hour and weekly limits you have used.
[Opus 5.5 · medium] ctx 45.2k/200.0k (23%) | in 1.2M out 34.5k | $1.23 | 5h 12% (3h 10m) | week 34% (2d 4h)
<!-- screenshot: add docs/screenshot.png here -->
| Part | Meaning |
|---|---|
[Opus 5.5 · medium] | The model, and the effort of its last request. Before the first request, the effort comes from your settings. |
ctx 45.2k/200.0k (23%) | How full the context window is. |
in 1.2M out 34.5k | Input and output tokens, added up over the session. /clear resets them. |
$1.23 | What the session has cost so far. |
5h 12% (3h 10m) | Your 5-hour usage limit: the share used, and the time until it resets. |
week 34% (2d 4h) | Your weekly usage limit: the share used, and the time until it resets. |
The limits show only on a Claude subscription. They show -- until the first reply of the session.
The line updates after each turn, when a limit moves by a whole point, and once a minute so the countdowns stay current.
Type this at the Claude Code prompt in a terminal:
/plugin install usage-status --marketplace aliahmadcse/claude-usage-status
Answer y to add the marketplace, then choose the user scope so every session loads the mod.
Mods (function hooks) are an early-access Claude Code feature. This mod was built and tested on Claude Code 2.1.296.
If you also have a statusLine command in ~/.claude/settings.json, remove it, or you will see two status lines.
The mod is one hooks module, hooks/register.ts:
session.measure and a one-minute timer read $.session.usage(): the context, the cost, and the five_hour and seven_day limit windows.turn.step records the effort of each main-thread request.turn.complete adds each turn's tokens to totals kept in $.state, so they survive a reload.$.ui.status() pins the line.All formatting is in hooks/format.ts, as pure functions.
claude plugin validate .
claude plugin test .
To run a local copy, start Claude Code with claude --plugin-dir <path to this folder>.
MIT
hooks/register.ts 95 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { UsageStatusTotals } from '../types'
5import { effortFromSettings, formatStatus } from './format'
6
7const ZERO: UsageStatusTotals = { tokensIn: 0, tokensOut: 0 }
8
9const totals = atom({ plugin: 'usage-status', key: 'totals' } as const, ZERO)
10const lastModel = atom({ plugin: 'usage-status', key: 'model' } as const, null)
11const lastEffort = atom({ plugin: 'usage-status', key: 'effort' } as const, null)
12
13/**
14 * Reads the engine's figures and pins the status line.
15 *
16 * @param $ the engine interface
17 */
18async function refresh($: EngineInterface): Promise<void> {
19 const [usage, sum, turnModel, turnEffort, now] = await Promise.all([
20 $.session.usage(),
21 read($, totals),
22 read($, lastModel),
23 read($, lastEffort),
24 $.clock.now(),
25 ])
26 const model = turnModel ?? (await $.session.model())
27 // Before the first request, fall back to the effort in settings.
28 const effort = turnEffort ?? effortFromSettings(await $.settings.read(), model)
29
30 $.ui.status(
31 formatStatus({
32 model,
33 effort,
34 contextTokens: usage.context.tokens,
35 contextWindow: usage.context.window,
36 contextPercent: usage.context.percent,
37 tokensIn: sum.tokensIn,
38 tokensOut: sum.tokensOut,
39 costUsd: usage.cost?.usd,
40 limits: usage.rateLimits,
41 now,
42 }),
43 )
44}
45
46/**
47 * The usage-status mod: a status line with the model, the context fill, the
48 * session's tokens and cost, and the 5-hour and weekly usage limits.
49 */
50export const register: Register = on => {
51 on('session.start', async ($, e, next) => {
52 const result = await next(e)
53 await refresh($)
54 // Keeps the reset countdowns current while the session is idle.
55 $.clock.every(60_000, () => refresh($))
56 return result
57 })
58
59 // Records the effort each main-thread request is sent with.
60 on('turn.step', async function* ($, e, next) {
61 if (e.agentId === undefined) await update($, lastEffort, () => e.effort ?? null)
62 return yield* next(e)
63 })
64
65 on('turn.complete', async ($, e, next) => {
66 const usage = e.usage
67 if (usage) {
68 await update($, totals, t => ({
69 tokensIn:
70 t.tokensIn +
71 usage.input_tokens +
72 usage.cache_read_input_tokens +
73 usage.cache_creation_input_tokens,
74 tokensOut: t.tokensOut + usage.output_tokens,
75 }))
76 if (e.agentId === undefined) await update($, lastModel, () => usage.model)
77 }
78 return next(e)
79 })
80
81 on('session.measure', async ($, e, next) => {
82 const result = await next(e)
83 await refresh($)
84 return result
85 })
86
87 on('session.end', async ($, e, next) => {
88 if (e.reason === 'clear') {
89 await update($, totals, () => ZERO)
90 await refresh($)
91 }
92 return next(e)
93 })
94}
95hooks/format.ts 153 lines1/**
2 * Pure formatting for the usage-status line. Nothing here touches the engine,
3 * so every function can be tested with plain values.
4 */
5
6/**
7 * One rate-limit window as `$.session.usage()` reports it.
8 */
9export interface LimitWindow {
10 /** `five_hour`, `seven_day`, or a gateway's `spend_limit`. */
11 kind: string
12 /** Share of the window used, 0 to 100. */
13 percentUsed: number
14 /** When the window resets, as an ISO 8601 timestamp. */
15 resetsAt?: string
16}
17
18/**
19 * Everything the status line shows, gathered from the engine.
20 */
21export interface StatusFigures {
22 /** The model's display name or API id, or null when unknown. */
23 model: string | null
24 /** The reasoning effort, as a level such as `medium` or a token budget. */
25 effort?: string | number
26 /** Input tokens of the last response, or undefined before the first one. */
27 contextTokens?: number
28 /** The model's context window in tokens. */
29 contextWindow: number
30 /** Context fill as a whole percentage, when known. */
31 contextPercent?: number
32 /** Input tokens summed over the session's turns. */
33 tokensIn: number
34 /** Output tokens summed over the session's turns. */
35 tokensOut: number
36 /** The session's cost in US dollars, when the host keeps a ledger. */
37 costUsd?: number
38 /** The account's rate-limit windows; empty off a subscription. */
39 limits: LimitWindow[]
40 /** The current time in milliseconds since the epoch. */
41 now: number
42}
43
44/**
45 * Formats a token count as 950, 12.3k, or 1.2M.
46 *
47 * @param n the token count
48 * @returns the short form
49 */
50export function formatTokens(n: number): string {
51 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
52 if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
53 return String(n)
54}
55
56/**
57 * Turns an API model id such as `claude-opus-5-5` into `Opus 5.5`. Any other
58 * name is returned as it is.
59 *
60 * @param model the model name or id
61 * @returns the display name
62 */
63export function formatModel(model: string): string {
64 const match = /^claude-([a-z]+)-(\d+)-(\d+)/.exec(model)
65 if (!match) return model
66 const [, family = '', major, minor] = match
67 return `${family.charAt(0).toUpperCase()}${family.slice(1)} ${major}.${minor}`
68}
69
70/**
71 * Formats the time until a reset as `45m`, `3h 10m`, or `2d 4h`.
72 *
73 * @param resetsAt the reset time as an ISO 8601 timestamp
74 * @param now the current time in milliseconds since the epoch
75 * @returns the time left, or undefined when the timestamp cannot be read
76 */
77export function formatTimeLeft(resetsAt: string, now: number): string | undefined {
78 const at = Date.parse(resetsAt)
79 if (Number.isNaN(at)) return undefined
80 const minutes = Math.max(0, Math.round((at - now) / 60_000))
81 const days = Math.floor(minutes / 1440)
82 const hours = Math.floor((minutes % 1440) / 60)
83 const mins = minutes % 60
84 if (days > 0) return `${days}d ${hours}h`
85 if (hours > 0) return `${hours}h ${mins}m`
86 return `${mins}m`
87}
88
89/**
90 * Formats one rate-limit window as `5h 12% (3h 10m)`.
91 *
92 * @param label the short name shown for the window
93 * @param window the window, or undefined when the engine has no reading yet
94 * @param now the current time in milliseconds since the epoch
95 * @returns the formatted part
96 */
97export function formatLimit(label: string, window: LimitWindow | undefined, now: number): string {
98 if (!window) return `${label} --`
99 const percent = `${Math.round(window.percentUsed)}%`
100 const left = window.resetsAt ? formatTimeLeft(window.resetsAt, now) : undefined
101 return left ? `${label} ${percent} (${left})` : `${label} ${percent}`
102}
103
104/**
105 * Builds the full status line, for example:
106 * `[Opus 5.5] ctx 45.2k/200.0k (23%) | in 1.2M out 34.5k | $1.23 | 5h 12% (3h 10m) | week 34% (2d 4h)`.
107 *
108 * @param f the figures to show
109 * @returns the status line text
110 */
111export function formatStatus(f: StatusFigures): string {
112 const parts: string[] = []
113
114 let model = f.model ? formatModel(f.model) : 'unknown'
115 if (f.effort !== undefined) model += ` · ${f.effort}`
116 const used = f.contextTokens === undefined ? '--' : formatTokens(f.contextTokens)
117 let ctx = `ctx ${used}/${formatTokens(f.contextWindow)}`
118 if (f.contextPercent !== undefined) ctx += ` (${Math.round(f.contextPercent)}%)`
119 parts.push(`[${model}] ${ctx}`)
120
121 parts.push(`in ${formatTokens(f.tokensIn)} out ${formatTokens(f.tokensOut)}`)
122
123 if (f.costUsd !== undefined) parts.push(`$${f.costUsd.toFixed(2)}`)
124
125 const session = f.limits.find(l => l.kind === 'five_hour')
126 const weekly = f.limits.find(l => l.kind === 'seven_day')
127 if (f.limits.length > 0) {
128 parts.push(formatLimit('5h', session, f.now))
129 parts.push(formatLimit('week', weekly, f.now))
130 }
131
132 return parts.join(' | ')
133}
134
135/**
136 * Reads the effort level from Claude Code settings: the per-model
137 * `modelSettings[model].effortLevel` first, then the global `effortLevel`.
138 *
139 * @param settings the merged settings object, as `$.settings.read()` gives it
140 * @param modelId the model's API id, or null when unknown
141 * @returns the effort level, or undefined when no setting exists
142 */
143export function effortFromSettings(settings: unknown, modelId: string | null): string | undefined {
144 if (typeof settings !== 'object' || settings === null) return undefined
145 const s = settings as {
146 effortLevel?: unknown
147 modelSettings?: Record<string, { effortLevel?: unknown } | undefined>
148 }
149 const perModel = modelId ? s.modelSettings?.[modelId]?.effortLevel : undefined
150 if (typeof perModel === 'string') return perModel
151 return typeof s.effortLevel === 'string' ? s.effortLevel : undefined
152}
153types/index.d.ts 23 lines1/**
2 * Token totals this mod adds up over the session's turns.
3 */
4export type UsageStatusTotals = {
5 /** Input tokens, cached and uncached together, summed over every turn. */
6 tokensIn: number
7 /** Output tokens summed over every turn. */
8 tokensOut: number
9}
10
11declare module 'claude-code' {
12 /**
13 * The values this mod keeps in `$.state`, so they survive a reload.
14 */
15 interface PluginState {
16 'usage-status': {
17 totals: UsageStatusTotals
18 model: string | null
19 effort: string | number | null
20 }
21 }
22}
23