SLOPSHOPPER

usage-status

Pins a status line under the prompt that labels the plan usage windows: 5h session and 7d week, as percent left.

statusprocesstimer
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: 5h session: 69% left (resets 9:53 AM) | ctx 49% | 8k tok | $0.42 | PC ?
README

usage-status: a labeled status line for Claude Code

The default footer shows bare numbers like 7d 59% left. This mod pins one clear line under the prompt instead:

5h session: 77% left (resets 9:40 PM) | 7d week: 59% left (resets Wed 4:00 PM) | ctx 18% | 212k tok | ~9k/1% est | $3.41 | PC ok
PieceWhat it is
5h session / 7d weekYour plan's rolling 5-hour and 7-day usage windows, as percent left, with the reset time. Whichever hits zero first locks you out.
ctxHow full the context window is.
tokTokens this session (fresh input + cache writes + output; cache reads left out because they inflate it).
~N/1% estRough tokens per 1% of the 5h window, shown once the meter has moved 2+ points. An estimate, never a quota.
$Session cost at API prices. On a flat-rate plan it is a "how heavy was this session" gauge, not your bill.
PC okEvery 60 s a read-only check (CPU, free RAM, free disk, ping 1.1.1.1). Names the problem if a drive is under 15% free, RAM under 10% free, CPU over 90%, or the internet is down. PC ? means the check itself failed.
NEW DAY: ...Shows up if your session runs past midnight, so work doesn't get filed under yesterday.

Install

  1. Put the usage-status folder somewhere permanent.
  2. Load it as a plugin folder, either with claude --plugin-dir <path-to-usage-status> or by adding its path to CLAUDE_CODE_PLUGIN_DIRS (;-separated on Windows, : elsewhere) in the env block of your settings.json.
  3. Restart Claude Code. Editing the mod later hot-reloads when loaded with --plugin-dir; with CLAUDE_CODE_PLUGIN_DIRS it needs a restart.

You can run claude plugin validate on the folder first.

Notes and limits

  • Status: running daily on Windows 11 since 2026-10-03. There is no unit test; it is proven by looking at the line.
  • The PC ok check is PowerShell / Windows only. On macOS or Linux, delete the health pieces (HEALTH_PS, pollHealth, the health part of buildLine) or replace the command; the usage, context, token and new-day pieces work anywhere.
  • The tsconfig.json points at type files that Claude Code generates in .claude-plugin/types/ when the plugin loads; you don't need to write them.
  • Plan limits only appear on a subscription plan. On API billing the 5h and 7d pieces simply don't show.
  • Read-only: the mod never changes your files or your machine.
Source 1 files
hooks/register.ts 158 lines
1import type { Engine, Register } from 'claude-code'
2
3type Limit = { kind: string; percentUsed: number; resetsAt?: string }
4
5const LABELS: readonly (readonly [string, string])[] = [
6  ['five_hour', '5h session'],
7  ['seven_day', '7d week'],
8]
9const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
10
11const HEALTH_MS = 60 * 1000
12const DATE_CHECK_MS = 5 * 60 * 1000
13const DISK_MIN = 15
14const RAM_MIN = 10
15const CPU_MAX = 90
16const EST_MIN_POINTS = 2
17
18const HEALTH_PS = [
19  '$c = (Get-CimInstance Win32_Processor | Measure-Object LoadPercentage -Average).Average',
20  '$o = Get-CimInstance Win32_OperatingSystem',
21  '$ram = [math]::Round(100 * $o.FreePhysicalMemory / $o.TotalVisibleMemorySize)',
22  '$disks = @(Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { [pscustomobject]@{ id = $_.DeviceID; free = [math]::Round(100 * $_.FreeSpace / $_.Size) } })',
23  '$net = [bool](Test-Connection 1.1.1.1 -Count 1 -Quiet)',
24  '[pscustomobject]@{ cpu = [math]::Round($c); ram = $ram; disks = $disks; net = $net } | ConvertTo-Json -Compress',
25].join('; ')
26
27let limits: readonly Limit[] = []
28let ctxPercent: number | undefined
29let costUsd: number | undefined
30let tokens = 0
31let health = 'PC ...'
32let baseline: { percent: number; tokens: number } | undefined
33let sessionDate = ''
34let dateRolled = false
35
36const compact = (n: number): string =>
37  n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
38
39const clockText = (d: Date): string => {
40  const h = d.getHours()
41  const m = d.getMinutes()
42  return (h % 12 || 12) + ':' + (m < 10 ? '0' : '') + m + (h < 12 ? ' AM' : ' PM')
43}
44
45const resetText = (kind: string, iso: string | undefined): string => {
46  if (!iso) return ''
47  const d = new Date(iso)
48  if (isNaN(d.getTime())) return ''
49  return kind === 'five_hour' ? clockText(d) : DAYS[d.getDay()] + ' ' + clockText(d)
50}
51
52// Calibration for the "~N/1% est" figure: tokens this session spent per point of the 5h window
53// used since we first saw it (re-based whenever the window resets and the percent drops).
54// Rough by design: other devices and cache behavior also move the meter.
55const track = (): void => {
56  const hit = limits.find((l) => l.kind === 'five_hour')
57  if (!hit) return
58  if (!baseline || hit.percentUsed < baseline.percent) baseline = { percent: hit.percentUsed, tokens }
59}
60
61const estimate = (): string | undefined => {
62  const hit = limits.find((l) => l.kind === 'five_hour')
63  if (!hit || !baseline) return undefined
64  const moved = hit.percentUsed - baseline.percent
65  const spent = tokens - baseline.tokens
66  if (moved < EST_MIN_POINTS || spent <= 0) return undefined
67  return '~' + compact(Math.round(spent / moved)) + '/1% est'
68}
69
70const localDate = (d: Date): string =>
71  d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
72
73const checkDate = ($: Engine): void => {
74  const now = localDate(new Date())
75  if (sessionDate && now !== sessionDate) {
76    dateRolled = true
77    sessionDate = now
78    render($)
79  }
80}
81
82const buildLine = (): string | undefined => {
83  const parts: string[] = []
84  if (dateRolled) parts.push('NEW DAY: the date changed since this session started')
85  for (const [kind, label] of LABELS) {
86    const hit = limits.find((l) => l.kind === kind)
87    if (!hit) continue
88    const reset = resetText(kind, hit.resetsAt)
89    parts.push(label + ': ' + Math.round(100 - hit.percentUsed) + '% left' + (reset ? ' (resets ' + reset + ')' : ''))
90  }
91  if (ctxPercent !== undefined) parts.push('ctx ' + Math.round(ctxPercent) + '%')
92  if (tokens > 0) parts.push(compact(tokens) + ' tok')
93  const est = estimate()
94  if (est) parts.push(est)
95  if (costUsd !== undefined) parts.push('$' + costUsd.toFixed(2))
96  parts.push(health)
97  return parts.join(' | ')
98}
99
100const render = ($: Engine): void => {
101  $.ui.status(buildLine())
102}
103
104const judge = (raw: string): string => {
105  const h = JSON.parse(raw) as { cpu: number; ram: number; disks: { id: string; free: number }[]; net: boolean }
106  const issues: string[] = []
107  for (const d of h.disks) if (d.free < DISK_MIN) issues.push(d.id + ' ' + d.free + '% free')
108  if (h.ram < RAM_MIN) issues.push('RAM ' + h.ram + '% free')
109  if (h.cpu > CPU_MAX) issues.push('CPU ' + h.cpu + '%')
110  if (!h.net) issues.push('internet down')
111  return issues.length > 0 ? 'PC: ' + issues.join(', ') : 'PC ok'
112}
113
114const pollHealth = async ($: Engine): Promise<void> => {
115  try {
116    const r = await $.process.run(['powershell', '-NoProfile', '-NonInteractive', '-Command', HEALTH_PS], {
117      timeoutMs: 20000,
118    })
119    health = r.exitCode === 0 ? judge(r.stdout) : 'PC ?'
120  } catch {
121    health = 'PC ?'
122  }
123  render($)
124}
125
126export const register: Register = (on) => {
127  on('session.start', async ($, e, next) => {
128    sessionDate = localDate(new Date())
129    dateRolled = false
130    const usage = await $.session.usage()
131    limits = usage.rateLimits
132    ctxPercent = usage.context.percent
133    costUsd = usage.cost?.usd
134    track()
135    render($)
136    void pollHealth($)
137    $.clock.every(HEALTH_MS, () => pollHealth($))
138    $.clock.every(DATE_CHECK_MS, () => checkDate($))
139    return next(e)
140  })
141
142  on('session.measure', ($, e, next) => {
143    limits = e.rateLimits
144    ctxPercent = e.context.percent
145    costUsd = e.cost?.usd
146    track()
147    render($)
148    return next(e)
149  })
150
151  on('turn.complete', ($, e, next) => {
152    const u = e.usage
153    if (u) tokens += u.input_tokens + u.cache_creation_input_tokens + u.output_tokens
154    render($)
155    return next(e)
156  })
157}
158