The Claude Code statusline is the single line rendered under the input box. The mechanism behind it is refreshingly simple: Claude Code runs a local command, pipes session state as JSON to its stdin, and displays the first line the command prints to stdout. That's the whole contract. Everything else — colors, segments, responsiveness — is your script's job.
The settings.json block
You enable it with a statusLine entry in ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "node ~/.claude/terminal-pro/terminal-pro-statusline.mjs",
"padding": 0,
"refreshInterval": 10
}
}command— any executable your shell can run. Use an absolute or~path; the working directory is not guaranteed.padding— horizontal padding around the line.0lets you use the full width.refreshInterval— seconds between re-runs. The line also refreshes on session events; 10 is a sane floor so you don't fork a process every second.
The JSON payload on stdin
Each run receives one JSON document. The useful fields, from a real session:
{
"model": { "id": "claude-opus-4-8", "display_name": "Opus 4.8" },
"effort": { "level": "high" },
"context_window": {
"context_window_size": 1000000,
"used_percentage": 26,
"remaining_percentage": 74
},
"rate_limits": {
"five_hour": { "used_percentage": 42, "resets_at": 1781290000 },
"seven_day": { "used_percentage": 36, "resets_at": 1781730000 }
},
"cost": {
"total_cost_usd": 0.38,
"total_duration_ms": 5400000,
"total_lines_added": 312,
"total_lines_removed": 42
},
"workspace": {
"current_dir": "/home/you/project",
"project_dir": "/home/you/project",
"git_worktree": "feature/polish",
"repo": { "host": "github.com", "owner": "you", "name": "project" }
},
"worktree": { "name": "project", "branch": "feature/polish", "path": "..." }
}Two practical rules. First, every field is optional in practice — a fresh session may have no rate_limits, a non-git directory has no repo. Guard every access. Second, percentages come pre-computed (used_percentage), so don't re-derive them from token counts.
A minimal working script
Paste this into ~/.claude/statusline.mjs and point the command at it:
#!/usr/bin/env node
let raw = "";
process.stdin.on("data", (c) => (raw += c));
process.stdin.on("end", () => {
let d = {};
try { d = JSON.parse(raw); } catch {}
const parts = [];
if (d.model?.display_name) parts.push(d.model.display_name);
if (d.context_window?.used_percentage != null)
parts.push(`ctx ${d.context_window.used_percentage}%`);
if (d.rate_limits?.five_hour?.used_percentage != null)
parts.push(`5h ${d.rate_limits.five_hour.used_percentage}%`);
console.log(parts.join(" | ") || "claude");
});Test it without Claude Code by piping sample JSON: echo '{"model":{"display_name":"Opus 4.8"}}' | node ~/.claude/statusline.mjs
Width: respect COLUMNS
Claude Code exposes the terminal width through the COLUMNS environment variable. A statusline that ignores it wraps — and a wrapped statusline is worse than none. Measure your assembled string and drop the least important segments until it fits:
const width = Number(process.env.COLUMNS) || 80;
let line = parts.join(" | ");
while (line.length > width && parts.length > 1) {
parts.pop();
line = parts.join(" | ");
}Why a local file beats a one-liner
You'll see setups that shell out to jq, curl, or even npxon every refresh. Avoid that: the statusline runs constantly, so network calls and package resolution add latency and failure modes to every prompt. A single local Node file with zero dependencies starts fast and works offline. That's exactly the model the statusline builder generates — a self-contained runtime under ~/.claude/terminal-pro/ plus the settings patch shown above.