SLOPSHOPPER

status-band

Band above the prompt: last two path segments, git branch, 5h and weekly rate-limit bars with percentages, model and session cost

newbandprocesstimer
v0.1.0MITupdated 2026-10-25golfomania/claude-status-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · status-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 ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ● work/app feat/auth-refresh │ 5h ███░░░░░░░ 31% │ Week – │ claude-opus-5-5 $0.42 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ● work/app feat/auth-refresh │ 5h ███░░░░░░░ 31% │ Week – │ claude-opus-5-5 $0.42 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
README

status-band

A Claude Code mod that draws a status band directly above the prompt, framed in a rounded green border:

╭──────────────────────────────────────────────────────────────────────────────────────╮
│ ● my-project/api main │ 5h ██░░░░░░░░ 20% ↻ 3h 10m │ Week █░░░░░░░░░ 12% ↻ 6d 21h │ claude-fable-5-1 $2.52 │
╰──────────────────────────────────────────────────────────────────────────────────────╯

What it shows, left to right:

  • Path and branch: the last two segments of the directory Claude Code was started in, and the current git branch (refreshed every 15 seconds). Outside a git repository it says no git.
  • 5h limit: a 10-cell bar and the percentage of the 5-hour rate-limit window used, plus the time until it resets. Green below 70 %, yellow from 70 %, red from 90 %.
  • Week limit: the same for the 7-day window.
  • Model and cost: the model the main loop runs, as /model shows it, and the session's cost in US dollars.

The rate-limit figures come from the API's own responses and update after every turn. They show – until the first response arrives, and stay empty on accounts without a subscription.

Install

At the prompt of a terminal session:

/plugin install status-band --marketplace golfomania/claude-status-band

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

From a shell:

claude plugin install status-band --marketplace golfomania/claude-status-band

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

Development

Run it straight from a checkout for one session:

claude --plugin-dir /path/to/claude-status-band

Check it with claude plugin validate . and type-check with tsc -p . once the engine has laid the types into .claude-plugin/types/.

License

MIT

Source 2 files
hooks/register.tsx 162 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Limit, Meta, Repo } from '../types'
5
6const repo = atom({ plugin: 'status-band', key: 'repo' } as const, null as Repo | null)
7const limits = atom({ plugin: 'status-band', key: 'limits' } as const, [] as Limit[])
8const meta = atom({ plugin: 'status-band', key: 'meta' } as const, { model: null, usd: null } as Meta)
9
10const BAR_WIDTH = 10
11const REFRESH_MS = 15_000
12
13const shortPath = (cwd: string): string =>
14  cwd.split('/').filter(Boolean).slice(-2).join('/') || '/'
15
16const toLimits = (rows: readonly SessionRateLimit[]): Limit[] =>
17  rows.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt }))
18
19const sameLimits = (a: Limit[], b: Limit[]): boolean =>
20  a.length === b.length &&
21  a.every((x, i) => {
22    const y = b[i]
23    return y !== undefined && x.kind === y.kind && x.percentUsed === y.percentUsed && x.resetsAt === y.resetsAt
24  })
25
26const colorFor = (pct: number): 'success' | 'warning' | 'error' =>
27  pct >= 90 ? 'error' : pct >= 70 ? 'warning' : 'success'
28
29const bar = (pct: number): { filled: string; empty: string } => {
30  const n = Math.max(0, Math.min(BAR_WIDTH, Math.round((pct / 100) * BAR_WIDTH)))
31  return { filled: '█'.repeat(n), empty: '░'.repeat(BAR_WIDTH - n) }
32}
33
34const resetsIn = (resetsAt: string | undefined, now: number): string | null => {
35  if (!resetsAt) return null
36  const ms = Date.parse(resetsAt) - now
37  if (!Number.isFinite(ms) || ms <= 0) return null
38  const minutes = Math.floor(ms / 60_000)
39  const days = Math.floor(minutes / 1440)
40  const hours = Math.floor((minutes % 1440) / 60)
41  const mins = minutes % 60
42  if (days > 0) return `${days}d ${hours}h`
43  if (hours > 0) return `${hours}h ${mins}m`
44  return `${mins}m`
45}
46
47async function refreshRepo($: EngineInterface, cwd: string): Promise<void> {
48  const result = await $.process
49    .run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd, timeoutMs: 5_000 })
50    .catch(() => null)
51  const branch = result && result.exitCode === 0 ? result.stdout.trim() || null : null
52  const next: Repo = { path: shortPath(cwd), branch }
53  const prev = await read($, repo)
54  if (prev && prev.path === next.path && prev.branch === next.branch) return
55  await update($, repo, () => next)
56}
57
58async function refreshLimits($: EngineInterface, rows: readonly SessionRateLimit[]): Promise<void> {
59  const next = toLimits(rows)
60  const prev = await read($, limits)
61  if (sameLimits(prev, next)) return
62  await update($, limits, () => next)
63}
64
65async function refreshMeta($: EngineInterface, usd: number | null | undefined): Promise<void> {
66  const model = await $.session.model().catch(() => null)
67  const prev = await read($, meta)
68  const next: Meta = { model, usd: usd ?? prev.usd }
69  if (prev.model === next.model && prev.usd === next.usd) return
70  await update($, meta, () => next)
71}
72
73const fmtUsd = (usd: number): string => (usd < 10 ? `$${usd.toFixed(2)}` : `$${usd.toFixed(1)}`)
74
75export const register: Register = on => {
76  on('session.start', async ($, e, next) => {
77    const started = await next(e)
78
79    await refreshRepo($, started.cwd)
80    const usage = await $.session.usage().catch(() => null)
81    if (usage) await refreshLimits($, usage.rateLimits)
82    await refreshMeta($, usage?.cost?.usd ?? null)
83
84    $.clock.every(REFRESH_MS, () => {
85      void (async () => {
86        const cwd = await $.session.cwd()
87        await refreshRepo($, cwd)
88        await refreshMeta($, undefined)
89      })()
90    })
91
92    return started
93  })
94
95  on('session.measure', async ($, e, next) => {
96    await refreshLimits($, e.rateLimits)
97    await refreshMeta($, e.cost?.usd ?? null)
98    return next(e)
99  })
100
101  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
102    if (e.props.hasSurvey) return next(e)
103
104    const [r, rows, m, now] = await Promise.all([read($, repo), read($, limits), read($, meta), $.clock.now()])
105    const { Box, Text } = $.ui.resolve(e)
106
107    const five = rows.find(l => l.kind === 'five_hour')
108    const week = rows.find(l => l.kind === 'seven_day')
109
110    const limit = (label: string, l: Limit | undefined) => {
111      if (!l) {
112        return (
113          <Box gap={1}>
114            <Text dimColor>{label}</Text>
115            <Text dimColor>–</Text>
116          </Box>
117        )
118      }
119      const pct = Math.round(l.percentUsed)
120      const color = colorFor(l.percentUsed)
121      const { filled, empty } = bar(l.percentUsed)
122      const reset = resetsIn(l.resetsAt, now)
123      return (
124        <Box gap={1}>
125          <Text weight="semibold">{label}</Text>
126          <Box>
127            <Text color={color}>{filled}</Text>
128            <Text dimColor>{empty}</Text>
129          </Box>
130          <Text color={color} tabular>
131            {pct}%
132          </Text>
133          {reset ? <Text dimColor>↻ {reset}</Text> : null}
134        </Box>
135      )
136    }
137
138    return (
139      <Box gap={2} borderStyle="round" borderColor="success" paddingX={1} alignSelf="flex-start">
140        <Box gap={1}>
141          <Text color={r?.branch ? 'success' : 'inactive'}>●</Text>
142          <Text dimColor>{r?.path ?? '…'}</Text>
143          {r?.branch ? (
144            <Text color="success">{r.branch}</Text>
145          ) : (
146            <Text dimColor>no git</Text>
147          )}
148        </Box>
149        <Text color="success">│</Text>
150        {limit('5h', five)}
151        <Text color="success">│</Text>
152        {limit('Week', week)}
153        <Text color="success">│</Text>
154        <Box gap={1}>
155          {m.model ? <Text color="claude">{m.model}</Text> : null}
156          {m.usd !== null ? <Text dimColor>{fmtUsd(m.usd)}</Text> : null}
157        </Box>
158      </Box>
159    )
160  })
161}
162
types/index.d.ts 10 lines
1export type Repo = { path: string; branch: string | null }
2export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
3export type Meta = { model: string | null; usd: number | null }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'status-band': { repo: Repo | null; limits: Limit[]; meta: Meta }
8  }
9}
10