SLOPSHOPPER

usage-band

Shows session tokens, weekly tokens and context usage (towards auto-compaction) in one line above the prompt.

newbandtimer
v0.1.0MITupdated 2026-10-15tobydoig/claude-usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› 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 Session tokens 31%, weekly tokens –, context 49% ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Session tokens 31%, weekly tokens –, context 49%
README

usage-band

A Claude Code mod that keeps your usage in view: one line above the prompt box.

Session tokens 60%, weekly tokens 71%, context 62%
  • Session tokens: how much of your plan's 5-hour allowance you've used.
  • Weekly tokens: how much of your plan's weekly allowance you've used.
  • Context: how far the conversation is towards auto-compaction, which triggers at 100%. With auto-compaction off, it shows how full the context window is instead.

A figure turns to the warning colour as it gets close: session or weekly at 80%, context at 90%. A figure not known yet shows as –. The [-] at the end of the band collapses it.

Why: in recent versions of the Claude desktop app, the ring next to the model picker shows account usage rather than context, and context usage moved into the Session Rail. This puts both back at a glance.

Install

At the prompt of a Claude Code terminal session:

/plugin install usage-band --marketplace tobydoig/claude-usage-band

Answer y to add the marketplace, then pick a scope (user installs it for every session).

From a shell:

claude plugin install usage-band --marketplace tobydoig/claude-usage-band

In the Claude desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with tobydoig/claude-usage-band, then Add plugin.

A mod installed at user scope from a terminal also loads in the desktop app's Code tab.

Where the figures come from

  • Context comes from Claude Code itself, measured after every reply, against the token count at which auto-compaction runs.
  • Session and weekly come from the plan usage figures Claude Code receives with each reply. When those aren't passed on (in testing, the desktop app's Code tab didn't pass them to mods), the mod reads the plan usage history the Claude desktop app keeps instead, re-read every minute:
  • Windows: %APPDATA%\Claude\plan-usage-history.json
  • macOS: ~/Library/Application Support/Claude/plan-usage-history.json

The desktop app writes a sample there about every 15 minutes while you're active, so these two figures can trail Settings → Usage by a few points. Without the desktop app or live figures, they show –.

Notes

  • Claude Code's plugin hooks API is early access and may change between releases.
  • Nothing leaves your machine: the mod reads local figures only and sends no requests.

Licence

MIT. See LICENSE.

Source 2 files
hooks/register.tsx 157 lines
1import { atom, read, update } from 'claude-code'
2import type { Hook, Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
3
4import type { UsageReading } from '../types'
5
6type Engine = Parameters<Hook<'session.measure'>>[0]
7
8const reading = atom({ plugin: 'usage-band', key: 'reading' } as const, null)
9
10// A figure turns to the warning colour at this much.
11const PLAN_WARN_PERCENT = 80
12const CONTEXT_WARN_PERCENT = 90
13
14// Re-reads the plan usage file each minute, so the plan figures stay current between turns.
15const REFRESH_MS = 60000
16
17const SESSION_WINDOW = 'five_hour'
18const WEEKLY_WINDOW = 'seven_day'
19
20// Where the desktop app records plan usage samples: { samples: [{ t, u: { fh, sd } }] }, fh the 5-hour and sd the 7-day window.
21const PLAN_USAGE_FILE = 'Claude/plan-usage-history.json'
22const MAC_APP_SUPPORT = 'Library/Application Support'
23
24type PlanUsage = { sessionPercent?: number; weeklyPercent?: number }
25
26// The token count at which auto-compaction runs, for the window it was measured against; only a new window size changes it.
27let compaction: { window: number; thresholdTokens?: number } | null = null
28
29// Whether the last measurement carried live plan figures; while it did, the plan usage file is not needed.
30let hasLivePlanUsage = false
31
32async function compactionThreshold($: Engine, context: SessionContextUsage) {
33  if (compaction?.window !== context.window) {
34    try {
35      // A summary breakdown is estimated locally, so this sends no request.
36      const { breakdown } = (await $.session.usage({ breakdown: 'summary' })).context
37      compaction = {
38        window: context.window,
39        thresholdTokens: breakdown?.isAutoCompactEnabled ? breakdown.autoCompactThreshold : undefined,
40      }
41    } catch {
42      // Without the threshold the band measures against the whole window; the next measurement tries again.
43      return undefined
44    }
45  }
46
47  return compaction.thresholdTokens
48}
49
50// The context's progress towards auto-compaction; against the whole window when there is no threshold.
51async function contextPercent($: Engine, context: SessionContextUsage) {
52  const threshold = await compactionThreshold($, context)
53  if (context.tokens === undefined || threshold === undefined) {
54    return context.percent
55  }
56
57  return Math.round((context.tokens / threshold) * 100)
58}
59
60// The plan usage file's newest sample; empty when there is no file or it cannot be read.
61async function planUsageFromFile($: Engine): Promise<PlanUsage> {
62  try {
63    const appData = await $.env.get('APPDATA')
64    const home = await $.env.get('HOME')
65    const folder = appData ?? (home === undefined ? undefined : `${home}/${MAC_APP_SUPPORT}`)
66    if (folder === undefined) {
67      return {}
68    }
69
70    const history = JSON.parse(await $.fs.read(`${folder}/${PLAN_USAGE_FILE}`)) as { samples?: { u?: { fh?: unknown; sd?: unknown } }[] }
71    const newest = history.samples?.at(-1)?.u
72
73    return {
74      sessionPercent: typeof newest?.fh === 'number' ? newest.fh : undefined,
75      weeklyPercent: typeof newest?.sd === 'number' ? newest.sd : undefined,
76    }
77  } catch {
78    return {}
79  }
80}
81
82// Live figures when the engine passes them on, which the desktop app does not always do; the plan usage file otherwise.
83async function planUsage($: Engine, rateLimits: SessionRateLimit[]): Promise<PlanUsage> {
84  const session = rateLimits.find(limit => limit.kind === SESSION_WINDOW)?.percentUsed
85  const weekly = rateLimits.find(limit => limit.kind === WEEKLY_WINDOW)?.percentUsed
86  hasLivePlanUsage = session !== undefined && weekly !== undefined
87  if (hasLivePlanUsage) {
88    return { sessionPercent: session, weeklyPercent: weekly }
89  }
90
91  const fromFile = await planUsageFromFile($)
92
93  return { sessionPercent: session ?? fromFile.sessionPercent, weeklyPercent: weekly ?? fromFile.weeklyPercent }
94}
95
96async function record($: Engine, context: SessionContextUsage, rateLimits: SessionRateLimit[]) {
97  const current: UsageReading = { ...(await planUsage($, rateLimits)), contextPercent: await contextPercent($, context) }
98  await update($, reading, () => current)
99}
100
101// Between turns only the plan usage file can move.
102async function refreshPlanUsage($: Engine) {
103  if (hasLivePlanUsage) {
104    return
105  }
106
107  const fromFile = await planUsageFromFile($)
108  await update($, reading, previous => ({ ...previous, ...fromFile }))
109}
110
111export const register: Register = on => {
112  on('session.start', async ($, e, next) => {
113    const result = await next(e)
114    $.clock.every(REFRESH_MS, () => void refreshPlanUsage($))
115
116    const usage = await $.session.usage()
117    await record($, usage.context, usage.rateLimits)
118
119    return result
120  })
121
122  on('session.measure', async ($, e, next) => {
123    await record($, e.context, e.rateLimits)
124
125    return next(e)
126  })
127
128  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
129    const current = await read($, reading)
130    if (e.props.hasSurvey || current === null) {
131      return next(e)
132    }
133
134    const { Box, Text } = $.ui.resolve(e)
135
136    return (
137      <Box>
138        <Text dimColor>
139          <Text color={warnAt(current.sessionPercent, PLAN_WARN_PERCENT)}>Session tokens {percent(current.sessionPercent)}</Text>
140          {', '}
141          <Text color={warnAt(current.weeklyPercent, PLAN_WARN_PERCENT)}>weekly tokens {percent(current.weeklyPercent)}</Text>
142          {', '}
143          <Text color={warnAt(current.contextPercent, CONTEXT_WARN_PERCENT)}>context {percent(current.contextPercent)}</Text>
144        </Text>
145      </Box>
146    )
147  })
148}
149
150function percent(value: number | undefined) {
151  return value === undefined ? '–' : `${value}%`
152}
153
154function warnAt(value: number | undefined, threshold: number) {
155  return value !== undefined && value >= threshold ? 'warning' : undefined
156}
157
types/index.d.ts 16 lines
1// What the band shows, each 0 to 100 and absent while unknown.
2export type UsageReading = {
3  // How much of the plan's 5-hour session allowance is used.
4  sessionPercent?: number
5  // How much of the plan's weekly allowance is used.
6  weeklyPercent?: number
7  // How far the context is towards auto-compaction, which runs at 100 (of the whole window when auto-compaction is off).
8  contextPercent?: number
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'usage-band': { reading: UsageReading | null }
14  }
15}
16