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

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
| Piece | What it is |
|---|---|
5h session / 7d week | Your plan's rolling 5-hour and 7-day usage windows, as percent left, with the reset time. Whichever hits zero first locks you out. |
ctx | How full the context window is. |
tok | Tokens this session (fresh input + cache writes + output; cache reads left out because they inflate it). |
~N/1% est | Rough 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 ok | Every 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. |
usage-status folder somewhere permanent.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.--plugin-dir; with CLAUDE_CODE_PLUGIN_DIRS it needs a restart.You can run claude plugin validate on the folder first.
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.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.5h and 7d pieces simply don't show.hooks/register.ts 158 lines1import 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