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

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
| Row | Shows |
|---|---|
| 1 | Model, working folder (~ for home), git branch, ✓ when the tree is clean or ●N for N changed files |
| 2 | Context 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.
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.
| Figure | Updated |
|---|---|
| Session time | every 1 s |
| Branch, changed files, model, folder | every 10 s and after each turn |
| Context %, cost | after each API response |
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.
MIT
hooks/register.tsx 161 lines1import { 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}
161types/index.d.ts 16 lines1export 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