SLOPSHOPPER

usage-status

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

newstatustimer
v0.2.0MITupdated 2026-10-10aliahmadcse/claude-usage-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-status
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ usage-status: [Opus 5.5] ctx 97.4k/200.0k (49%) | in 97.4k out 1.5k | $0.42 | 5h 31% | week --
README

usage-status

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 -->

What each part means

PartMeaning
[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.5kInput and output tokens, added up over the session. /clear resets them.
$1.23What 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.

Install

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.

How it works

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.

Develop

claude plugin validate .
claude plugin test .

To run a local copy, start Claude Code with claude --plugin-dir <path to this folder>.

License

MIT

Source 3 files
hooks/register.ts 95 lines
1import { 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}
95
hooks/format.ts 153 lines
1/**
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}
153
types/index.d.ts 23 lines
1/**
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