Status line for Claude Code: model, project, git, context, usage limits or API cost, duration. Works in the terminal and the desktop app.

A Claude Code status line that works in the terminal and the desktop app.
[Opus 5.5] π my-project | πΏ main β | β .claude
ββββββββββ 25% | Sess 40% β»6:23PM Wk 10% β»Fri 6PM | +12 -3 | β± 1m 5s
~/.claude/cost-tracking.The context bar turns yellow at 60% and red at 80%, with a hint to compact or hand off.
In the desktop app the mod shows this as a single plain line under the prompt, leaving out what the app already shows (branch, context, usage limits, lines changed) and the local site link.
Needs bash and jq.
Run these in Claude Code:
/plugin marketplace add dblanken-yale/statusline
/plugin install statusline@statusline
Or from a terminal:
claude plugin marketplace add dblanken-yale/statusline
claude plugin install statusline@statusline
That covers the desktop app. For the terminal, also point statusLine in ~/.claude/settings.json at the script the marketplace keeps:
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/plugins/marketplaces/statusline/statusline.sh"
}
}
Start a new Claude Code session.
Create ~/.claude/statusline.conf (or point CLAUDE_STATUSLINE_CONFIG at another file). The script sources it after its defaults, so anything you set there wins and survives updates:
THRESHOLD_YELLOW=50
ICON_PROJECT="dir:"
detect_site_url() { :; } # turn off the local site link
See the CONFIG block at the top of statusline.sh for everything you can set.
Turn on auto-update for the statusline marketplace in /plugin (Marketplaces tab), and new versions install when Claude Code starts. To update by hand:
claude plugin marketplace update statusline
The terminal status line reads the marketplace copy, so it updates along with it.
git clone git@github.com:dblanken-yale/statusline.git ~/code/statusline
claude --plugin-dir ~/code/statusline
Disable the marketplace install while you do this, or the mod runs twice. To try script changes in the terminal, point statusLine at ~/code/statusline/statusline.sh.
Bump version in .claude-plugin/plugin.json with every release. Installed copies only update when the version changes.
To check it after editing: claude plugin validate ~/code/statusline and claude plugin test ~/code/statusline.
hooks/statusline.mjs 80 lines1import { atom, read, update } from 'claude-code'
2
3// Shows the bundled statusline.sh output through $.ui.status on the desktop app.
4const ANSI = /\x1b\[[0-9;]*m/g
5const OSC8 = /\x1b\]8;;[^\x07]*\x07/g
6// Segments the desktop app already shows (git branch, context bar, Sess/Wk usage), the
7// +N -N count, which we cannot supply, and the site link, which plain status text cannot
8// make clickable.
9const DROP = /^(πΏ |[ββ]+ \d+%|(Sess|Wk) |\+\d+ -\d+$|π )/
10const MODEL = /^\[[^\]]*\]\s*/
11
12const activeMs = atom({ plugin: 'statusline', key: 'activeMs' }, 0)
13
14async function refresh($, usage) {
15 try {
16 // The terminal already runs the script through settings.json statusLine.
17 if ((await $.session.surfaces()).includes('terminal')) return
18 const { context, rateLimits, cost } = usage
19 const percent = context?.percent
20 const input = {
21 workspace: { current_dir: await $.session.cwd() },
22 model: { display_name: await $.session.model() },
23 session_id: await $.session.id(),
24 cost: {
25 total_cost_usd: cost?.usd ?? 0,
26 active_duration_ms: await read($, activeMs),
27 },
28 context_window: {
29 ...(percent !== undefined && { used_percentage: percent, remaining_percentage: 100 - percent }),
30 },
31 }
32 const limits = {}
33 for (const r of rateLimits ?? []) {
34 if (r.kind !== 'five_hour' && r.kind !== 'seven_day') continue
35 limits[r.kind] = { used_percentage: r.percentUsed }
36 if (r.resetsAt) limits[r.kind].resets_at = Math.floor(Date.parse(r.resetsAt) / 1000)
37 }
38 if (Object.keys(limits).length) input.rate_limits = limits
39
40 const res = await $.process.run(['bash', `${$.plugin.root}/statusline.sh`], {
41 stdin: JSON.stringify(input),
42 timeoutMs: 5000,
43 })
44 if (res.exitCode !== 0) return
45 const text = res.stdout
46 .replace(OSC8, '')
47 .replace(ANSI, '')
48 .split(/\n|\s+\|\s+/)
49 .map(seg => seg.trim().replace(MODEL, ''))
50 .filter(seg => seg && !DROP.test(seg))
51 .join(' | ')
52 await $.ui.status(text || undefined)
53 } catch {
54 // keep the previous status
55 }
56}
57
58export function register(on) {
59 on('session.start', async ($, e, next) => {
60 try {
61 await refresh($, await $.session.usage())
62 } catch {}
63 return next(e)
64 })
65 on('turn.complete', async ($, e, next) => {
66 const r = await next(e)
67 if (!e.agentId) {
68 await update($, activeMs, ms => ms + e.durationMs)
69 try {
70 await refresh($, await $.session.usage())
71 } catch {}
72 }
73 return r
74 })
75 on('session.measure', async ($, e, next) => {
76 await refresh($, e)
77 return next(e)
78 })
79}
80types/index.d.ts 9 lines1declare module 'claude-code' {
2 interface PluginState {
3 statusline: {
4 /** Sum of main-loop turn durations, in ms (desktop only). */
5 activeMs: number
6 }
7 }
8}
9