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.
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 1,
"refreshInterval": 10
}
}paddingadds characters of indent. It is optional and defaults to 0.refreshIntervalre-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.hideVimModeIndicatorhides the built-in INSERT marker when your script showsvim.modeitself.- A separate
subagentStatusLinesetting 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.idandmodel.display_name, for exampleclaude-opus-5-5andOpus.workspace.current_dir(alsocwd),workspace.project_dirwhere the session started, andworkspace.added_dirs.cost.total_cost_usd,cost.total_duration_ms,cost.total_api_duration_ms,cost.total_lines_addedandcost.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, andcurrent_usagebroken down by cache reads and writes.effort.level,thinking.enabledandfast_mode.session_id,session_name,transcript_path,versionandoutput_style.name.- Fields that appear only sometimes:
rate_limits(five-hour and seven-day use on Pro and Max),prompt_cache,prfor an open pull request,worktree,vimandagent.
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.
#!/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##*/}"#!/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}"#!/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))"#!/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.levelgives 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_percentageand.seven_day.used_percentageshow how much of each window you have used. Use// emptyin jq so the segment disappears when the field is missing. - Pull request:
.pr.numberand.pr.review_stateappear while the branch has an open pull request. - Cache:
.prompt_cache.warmsays 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
tputdoes not work, because output is captured; read theCOLUMNSvariable 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.
"statusLine": {
"type": "command",
"command": "powershell -NoProfile -File C:/Users/you/.claude/statusline.ps1"
}$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 0The 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:
#!/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.
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.jsonWhen 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, ordisableAllHooksis on. Then only a status line from managed settings runs. - A Windows path with backslashes in
command. Use forward slashes. - Still stuck:
claude --debuglogs 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.