SLOPSHOPPER

status-band

A two-row status band above the prompt: model, folder, git branch, context use, cost and session time.

newbandprocesstimer
v0.1.0MITupdated 2026-10-26LeoBaro/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 Opus 5.5 | /work/app | feat/auth-refresh | ●2 ctx ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ | 49% | $0.42 | 30m 0s ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Opus 5.5 | /work/app | feat/auth-refresh | ●2 ctx ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ | 49% | $0.42 | 30m 0s
README

status-band

A Claude Code mod that draws a two-row status band above the prompt:

 Opus 5.5  |  ~/projects/my-app  |  feature/auth-refactor  | ✓
ctx ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱ | 47% | $0.42 | 8m 47s
RowShows
1Model, working folder (~ for home), git branch, ✓ when the tree is clean or ●N for N changed files
2Context window bar, context used (%), session cost (USD), session time

It works wherever Claude Code draws: the terminal, the VS Code extension and the desktop app.

Install

At the prompt of a Claude Code terminal session:

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

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

From a shell:

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

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

Refresh rates

FigureUpdated
Session timeevery 1 s
Branch, changed files, model, folderevery 10 s and after each turn
Context %, costafter each API response

Develop

claude --plugin-dir .          # run a session with this folder loaded; edits hot-reload
claude plugin validate .
claude plugin test .

The mod uses Claude Code's early-access plugin API, so a future release may need small changes.

License

MIT

Source 2 files
hooks/register.tsx 161 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Info } from '../types'
5
6const info = atom({ plugin: 'status-band', key: 'info' } as const, null)
7const now = atom({ plugin: 'status-band', key: 'now' } as const, 0)
8
9const BAR_CELLS = 16
10const GIT_EVERY_MS = 10_000
11
12// "claude-opus-5-5" -> "Opus 5.5"; anything else is shown as given.
13function prettyModel(id: string): string {
14  const bare = id.replace(/\[.*\]$/, '').trim()
15  const full = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/i.exec(bare)
16  const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
17  if (full?.[1] && full[2]) {
18    return `${cap(full[1])} ${full[2]}${full[3] ? `.${full[3]}` : ''}`
19  }
20  if (/^(opus|sonnet|haiku|fable)$/i.test(bare)) return cap(bare)
21  return bare
22}
23
24function shortPath(path: string): string {
25  return path.replace(/^\/(home|Users)\/[^/]+/, '~')
26}
27
28function bar(percent: number): string {
29  const filled = Math.max(0, Math.min(BAR_CELLS, Math.round((percent / 100) * BAR_CELLS)))
30  return '▰'.repeat(filled) + '▱'.repeat(BAR_CELLS - filled)
31}
32
33function duration(ms: number): string {
34  const total = Math.max(0, Math.floor(ms / 1000))
35  const h = Math.floor(total / 3600)
36  const m = Math.floor((total % 3600) / 60)
37  const s = total % 60
38  if (h > 0) return `${h}h ${m}m`
39  if (m > 0) return `${m}m ${s}s`
40  return `${s}s`
41}
42
43async function git($: EngineInterface, cwd: string): Promise<Pick<Info, 'branch' | 'dirty'>> {
44  try {
45    const [head, status] = await Promise.all([
46      $.process.run(['git', 'branch', '--show-current'], { cwd, timeoutMs: 5000 }),
47      $.process.run(['git', 'status', '--porcelain'], { cwd, timeoutMs: 5000 }),
48    ])
49    if (head.exitCode !== 0) return { branch: null, dirty: 0 }
50    let branch = head.stdout.trim()
51    if (branch === '') {
52      const sha = await $.process.run(['git', 'rev-parse', '--short', 'HEAD'], { cwd, timeoutMs: 5000 })
53      branch = sha.exitCode === 0 ? `@${sha.stdout.trim()}` : 'detached'
54    }
55    const dirty = status.stdout.split('\n').filter(line => line.trim() !== '').length
56    return { branch, dirty }
57  } catch {
58    return { branch: null, dirty: 0 }
59  }
60}
61
62// Reads every figure afresh and stores it for the band.
63async function refresh($: EngineInterface): Promise<void> {
64  const [model, cwd, usage] = await Promise.all([
65    $.session.model(),
66    $.session.cwd(),
67    $.session.usage(),
68  ])
69  const repo = await git($, cwd)
70  const next: Info = {
71    model: prettyModel(model),
72    cwd: shortPath(cwd),
73    ...repo,
74    percent: usage.context.percent ?? null,
75    usd: usage.cost?.usd ?? null,
76    startedAt: usage.startedAt,
77  }
78  await update($, info, () => next)
79  const t = await $.clock.now()
80  await update($, now, () => t)
81}
82
83export const register: Register = on => {
84  on('session.start', async ($, e, next) => {
85    const result = await next(e)
86    await refresh($)
87    $.clock.every(1000, () => {
88      void $.clock.now().then(t => update($, now, () => t))
89    })
90    $.clock.every(GIT_EVERY_MS, () => {
91      void refresh($)
92    })
93    return result
94  })
95
96  on('turn.complete', async ($, e, next) => {
97    const result = await next(e)
98    await refresh($)
99    return result
100  })
101
102  // Context fill and cost move after each API response.
103  on('session.measure', async ($, e, next) => {
104    const result = await next(e)
105    await update($, info, old =>
106      old === null
107        ? old
108        : { ...old, percent: e.context.percent ?? old.percent, usd: e.cost?.usd ?? old.usd },
109    )
110    return result
111  })
112
113  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
114    if (e.props.hasSurvey) return next(e)
115    const data = await read($, info)
116    if (data === null) return next(e)
117    const t = await read($, now)
118    const { Box, Text } = $.ui.resolve(e)
119    const sep = <Text dimColor> | </Text>
120
121    return (
122      <Box flexDirection="column">
123        <Text>
124          <Text backgroundColor="#6b3fa0" color="#ffffff" bold>
125            {` ${data.model} `}
126          </Text>
127          {sep}
128          <Text backgroundColor="#1f5f8b" color="#ffffff">
129            {` ${data.cwd} `}
130          </Text>
131          {data.branch !== null ? (
132            <Text>
133              {sep}
134              <Text backgroundColor="#3b5323" color="#d4f0a0" italic>
135                {` ${data.branch} `}
136              </Text>
137              {sep}
138              {data.dirty === 0 ? (
139                <Text color="green">✓</Text>
140              ) : (
141                <Text color="yellow">{`●${data.dirty}`}</Text>
142              )}
143            </Text>
144          ) : null}
145        </Text>
146        <Text>
147          <Text color="cyan">{`ctx ${bar(data.percent ?? 0)}`}</Text>
148          {sep}
149          <Text bold>{data.percent === null ? '–' : `${Math.round(data.percent)}%`}</Text>
150          {sep}
151          <Text color="yellow">{data.usd === null ? '$–' : `$${data.usd.toFixed(2)}`}</Text>
152          {sep}
153          <Text color="magenta" italic>
154            {duration(t - data.startedAt)}
155          </Text>
156        </Text>
157      </Box>
158    )
159  })
160}
161
types/index.d.ts 16 lines
1export type Info = {
2  model: string
3  cwd: string
4  branch: string | null
5  dirty: number
6  percent: number | null
7  usd: number | null
8  startedAt: number
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'status-band': { info: Info | null; now: number }
14  }
15}
16