~ $ man how-statusline-works

How the Claude Code statusLine actually works

7 min · updated

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
  }
}

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.