Claude Code Statusline: Show Model, Cost, Branch and Context

The status line is one command in your settings that turns session data into a line under the prompt. What Claude Code sends it, when it refreshes, and four tested scripts for bash, plus PowerShell and Node versions for Windows.

7 min read

The Claude Code statusline is a row at the bottom of the terminal that shows whatever a script of yours prints. You point the statusLine setting at a command; Claude Code runs it with a JSON description of the session on standard input, and displays its standard output. The JSON carries the model, working directory, estimated cost, lines changed and context-window use, so the usual line of model, branch, context percentage and cost is a few lines of bash. The quickest start is /statusline followed by what you want to see.

The setting

Add a statusLine object to ~/.claude/settings.json for yourself, or to a project’s settings to share it. Anthropic’s status line documentation (opens in a new tab) gives the shape: type is always "command", and command is a script path or an inline shell command.

~/.claude/settings.json
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 1,
    "refreshInterval": 10
  }
}
  • padding adds characters of indent. It is optional and defaults to 0.
  • refreshInterval re-runs the command every N seconds, minimum 1, on top of the normal triggers. Set it for a clock, or for git state that background subagents change while the main session is idle; leave it out otherwise.
  • hideVimModeIndicator hides the built-in INSERT marker when your script shows vim.mode itself.
  • A separate subagentStatusLine setting formats the rows in the subagent panel. It receives a list of tasks rather than the session object.

Or let Claude write it. The commands reference (opens in a new tab) describes /statusline as taking a description of what you want, or no argument to build one from your shell prompt: /statusline show model, git branch and context percentage writes a script in ~/.claude/ and updates your settings, and /statusline remove it takes it away again.

What the script receives

Every run gets one JSON object on stdin. The fields most people use:

  • model.id and model.display_name, for example claude-opus-5-5 and Opus.
  • workspace.current_dir (also cwd), workspace.project_dir where the session started, and workspace.added_dirs.
  • cost.total_cost_usd, cost.total_duration_ms, cost.total_api_duration_ms, cost.total_lines_added and cost.total_lines_removed.
  • context_window.used_percentage, remaining_percentage, context_window_size (200,000 by default, 1,000,000 on models with the larger window), the token totals, and current_usage broken down by cache reads and writes.
  • effort.level, thinking.enabled and fast_mode.
  • session_id, session_name, transcript_path, version and output_style.name.
  • Fields that appear only sometimes: rate_limits (five-hour and seven-day use on Pro and Max), prompt_cache, pr for an open pull request, worktree, vim and agent.

Scripts have to cope with gaps. used_percentage and current_usage are null early in a session and just after /compact, and the optional objects are simply absent. Treat the cost as an estimate: the documentation says it is computed on your machine at list price and may differ from your bill, and Anthropic’s cost documentation (opens in a new tab) adds that on Pro and Max, where usage is included in the plan, the figure is not relevant for billing. Real usage is on /usage, covered in how to reduce Claude Code token usage.

When it refreshes

The script runs when a session starts or resumes, then after each assistant message, when /compact finishes, when the permission mode or vim mode changes, when a rate-limit window or warm prompt cache reaches its expiry time, and on your refreshInterval. Updates are debounced at 300 milliseconds, and a slow run is cancelled when a new one starts. The line runs locally and uses no tokens, but it goes blank if the script exits non-zero or prints nothing, so keep it fast and make every field optional.

Four scripts for bash

These use jq (opens in a new tab) to read the JSON, as Anthropic’s examples do. Save one as ~/.claude/statusline.sh, run chmod +x on it, and point statusLine.command at it. Each was tested by piping sample JSON into it, shaped as the documentation describes, including an early-session sample with null context values and a Windows path.

1. Model and folder: [Opus] my-app
#!/bin/bash
input=$(cat)
MODEL=$(printf '%s' "$input" | jq -r '.model.display_name // "?"')
DIR=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd')
DIR=${DIR//\\//}   # Windows paths arrive with backslashes
echo "[$MODEL] ${DIR##*/}"
2. Git branch, with * for uncommitted changes: [Opus] my-app on main*
#!/bin/bash
input=$(cat)
MODEL=$(printf '%s' "$input" | jq -r '.model.display_name // "?"')
DIR=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd')
BRANCH=$(git -C "$DIR" branch --show-current 2>/dev/null)
DIRTY=""
if [ -n "$BRANCH" ] && [ -n "$(git -C "$DIR" status --porcelain 2>/dev/null | head -n 1)" ]; then
  DIRTY="*"
fi
DIR=${DIR//\\//}
echo "[$MODEL] ${DIR##*/}${BRANCH:+ on $BRANCH$DIRTY}"
3. Context use as a bar: green, amber from 70%, red from 90%
#!/bin/bash
input=$(cat)
MODEL=$(printf '%s' "$input" | jq -r '.model.display_name // "?"')
PCT=$(printf '%s' "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
SIZE=$(printf '%s' "$input" | jq -r '.context_window.context_window_size // 200000')
BAR=""
for i in 1 2 3 4 5 6 7 8 9 10; do
  if [ $((i * 10)) -le "$PCT" ]; then BAR="${BAR}#"; else BAR="${BAR}-"; fi
done
if [ "$PCT" -ge 90 ]; then C='\033[31m'; elif [ "$PCT" -ge 70 ]; then C='\033[33m'; else C='\033[32m'; fi
printf '[%s] %b%s %s%%\033[0m of %sk\n' "$MODEL" "$C" "$BAR" "$PCT" "$((SIZE / 1000))"
4. Estimated cost, time and lines changed: [Opus] ~$0.01 | 0m 45s | +156 -23
#!/bin/bash
input=$(cat)
MODEL=$(printf '%s' "$input" | jq -r '.model.display_name // "?"')
COST=$(printf '%s' "$input" | jq -r '.cost.total_cost_usd // 0')
MS=$(printf '%s' "$input" | jq -r '.cost.total_duration_ms // 0' | cut -d. -f1)
ADDED=$(printf '%s' "$input" | jq -r '.cost.total_lines_added // 0')
REMOVED=$(printf '%s' "$input" | jq -r '.cost.total_lines_removed // 0')
printf '[%s] ~$%.2f | %dm %02ds | +%s -%s\n' "$MODEL" "$COST" $((MS / 60000)) $(((MS % 60000) / 1000)) "$ADDED" "$REMOVED"

Git calls are the slow part in a large repository. If the line lags, cache the branch in a temporary file and refresh it every few seconds, which is the pattern Anthropic’s caching example uses.

More to show, and how to lay it out

  • Effort: .effort.level gives low to max, and the object is absent on models without an effort setting, so fall back to nothing.
  • Plan limits: on Pro and Max, .rate_limits.five_hour.used_percentage and .seven_day.used_percentage show how much of each window you have used. Use // empty in jq so the segment disappears when the field is missing.
  • Pull request: .pr.number and .pr.review_state appear while the branch has an open pull request.
  • Cache: .prompt_cache.warm says whether the next message will read from the prompt cache or pay to rebuild it.
  • Layout: each line your script prints becomes a row, ANSI codes give colour, and OSC 8 sequences make clickable links in terminals that support them. Width detection with tput does not work, because output is captured; read the COLUMNS variable Claude Code sets instead.

Windows: PowerShell or Node

On Windows, Claude Code runs the command through Git Bash when it is installed and through PowerShell otherwise. Write paths in command with forward slashes, because Git Bash strips backslashes, and call PowerShell scripts through powershell, which works either way. The setup itself is covered in Claude Code on Windows.

settings.json on Windows
"statusLine": {
  "type": "command",
  "command": "powershell -NoProfile -File C:/Users/you/.claude/statusline.ps1"
}
statusline.ps1: [Opus] my-app on main | ctx 8% | ~$0.01
$data = $input | Out-String | ConvertFrom-Json
$dir = $data.workspace.current_dir
$line = "[$($data.model.display_name)] $(Split-Path $dir -Leaf)"
$branch = git -C $dir branch --show-current 2>$null
if ($branch) { $line += " on $branch" }
$pct = [math]::Floor([double]$data.context_window.used_percentage)
$line += " | ctx $pct%"
$line += " | ~`$" + ([double]$data.cost.total_cost_usd).ToString('0.00')
Write-Output $line
exit 0

The exit 0 matters: a failed git call outside a repository would otherwise leave a non-zero exit code, and the line would go blank. For one script that works on macOS, Linux and Windows alike, Node needs no jq:

statusline.js: "command": "node ~/.claude/statusline.js"
#!/usr/bin/env node
const { execFileSync } = require('child_process');
let input = '';
process.stdin.on('data', (chunk) => (input += chunk));
process.stdin.on('end', () => {
  const d = JSON.parse(input);
  const dir = d.workspace?.current_dir ?? d.cwd ?? '';
  let branch = '';
  try {
    branch = execFileSync('git', ['-C', dir, 'branch', '--show-current'], {
      encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1000,
    }).trim();
  } catch {}
  const pct = Math.floor(d.context_window?.used_percentage ?? 0);
  const steps = Math.min(10, Math.floor(pct / 10));
  const bar = '#'.repeat(steps) + '-'.repeat(10 - steps);
  const cost = (d.cost?.total_cost_usd ?? 0).toFixed(2);
  const folder = dir.split(/[\\/]/).pop();
  const where = folder + (branch ? ` on ${branch}` : '');
  console.log(`[${d.model?.display_name ?? '?'}] ${where} | ${bar} ${pct}% | ~$${cost}`);
});

Test before you wire it up

Pipe a sample object into the script and read what comes out. Try one with used_percentage set to null as well, since that is what the first render of every session sends.

Terminal
echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/home/you/my-app"},"context_window":{"used_percentage":25},"cost":{"total_cost_usd":0.42}}' | ~/.claude/statusline.sh
Get-Content sample.json | powershell -NoProfile -File statusline.ps1
node statusline.js < sample.json

When the line does not appear

  • The folder is not trusted yet. The command runs under the same workspace-trust rule (opens in a new tab) as hooks, so accept the trust prompt and restart.
  • The script is not executable, prints to stderr, or exits non-zero. Run it by hand with sample JSON.
  • Your organisation sets allowManagedHooksOnly, or disableAllHooks is on. Then only a status line from managed settings runs.
  • A Windows path with backslashes in command. Use forward slashes.
  • Still stuck: claude --debug logs the script’s stderr on each run.

The line shows the session, not the work

A status line tells you what this session is doing: which model, how full the context is, which branch. It cannot tell a teammate what you are working on. That lives on a shared board. With fenbs connected over MCP, Claude Code can read the task it is working on and comment on it when it stops, and every change is recorded in History under the name of the person whose assistant made it. A useful habit is to name the session after the task with /rename, so session_name in the JSON matches the task you see on the board.

Related

Every slash command in one list: Claude Code commands cheat sheet. What the model and effort fields mean: Claude Sonnet vs Opus. Running scripts on events rather than on refresh: Claude Code hooks that update a board. Connecting the board: Claude Code and fenbs.

Questions people ask.

How do I add a status line to Claude Code?

Run /statusline with a description of what you want, and Claude Code writes a script and updates your settings. Or add a statusLine object with type set to command and command set to your script path in ~/.claude/settings.json.

Does the Claude Code status line use tokens?

No. The script runs locally and uses no API tokens. It runs after each assistant message and on a few other events, so it should be fast.

Is the cost in the status line what I will be billed?

Not exactly. cost.total_cost_usd is estimated on your machine at list price and may differ from your bill. On Pro and Max, usage is included in the subscription, so treat it as a relative measure rather than a charge.

Why does my Claude Code status line not show on Windows?

The usual causes are backslashes in the command path, which Git Bash strips, a folder you have not yet trusted, or a script that exits with an error. Use forward slashes, accept the trust prompt, and test the script with sample JSON.

Start with one thing.

There is nothing to set up first. Write one line and you’ve started.