Claude Code statusline: setup and examples
Updated 24 October 2026
Claude Code’s status line (the statusline, as most people type it) is a row at the bottom of the interface that shows whatever a script of yours prints. Claude Code runs the script with a JSON description of the session on stdin: model, folder, cost, context usage, rate limits and more. You can have Claude write the script for you with /statusline, or write it yourself. Below are both routes, every field the script receives, when it runs, five scripts to copy, and fixes for a blank status line.
Quick answer
| You want to | Do this |
|---|---|
| Get one fast | /statusline show model, folder and context % |
| Configure it yourself | statusLine in ~/.claude/settings.json, see below |
| Show model and folder | Example 1 |
| Show the git branch | Example 2 |
| Show context and cost, in color | Example 3 |
| Show rate limits, session name, worktree | Example 4 |
Skip installing jq | Example 5 (Python) |
| Remove it | /statusline delete, or delete the statusLine key |
1. Let Claude write it: /statusline
Type /statusline followed by what you want to see:
Claude Code/statusline show model name, git branch and context percentage with a progress bar
Claude Code writes a script into ~/.claude/ and adds the statusLine setting for you. Approve the file edits when it asks. To change it later, run /statusline again with new instructions. To remove it, run /statusline delete (clear or remove it work too).
This is the quickest route, and the result is an ordinary script you can open and edit. The rest of this page is for when you want to know what it did or write your own.
2. Configure it by hand
Add a statusLine object to your user settings, ~/.claude/settings.json, to use it in every project, or to a project’s .claude/settings.json for that project only:
~/.claude/settings.json{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0
}
}
| Field | What it does |
|---|---|
type | Always "command". |
command | A script path or an inline shell command. It runs in a shell, so pipes and ~ work. |
padding | Extra horizontal spacing in characters, on top of Claude Code’s own. Default 0. |
refreshInterval | Also re-run the command every N seconds (minimum 1). Leave it out to run only on events. |
hideVimModeIndicator | true hides the built-in -- INSERT -- text, for scripts that print vim.mode themselves. |
For a one-liner you don’t need a script file. This prints the model and the folder name with jq:
~/.claude/settings.json{
"statusLine": {
"type": "command",
"command": "jq -r '\"[\\(.model.display_name)] \\(.workspace.current_dir | split(\"/\") | last)\"'"
}
}
Claude Code reloads settings when you save the file, so the status line appears without a restart. It sits on its own row above the footer badges. With a custom status line, Claude Code hides most footer hints, such as esc to interrupt and ? for shortcuts.
3. The JSON your script receives
Every run, Claude Code pipes one JSON object to your script’s stdin. These are the fields you’ll use most:
| Field | Contains |
|---|---|
model.id, model.display_name | Model ID and short name, such as Opus |
workspace.current_dir (same as cwd) | Current working directory |
workspace.project_dir | Where Claude Code was started; can differ from the current directory |
workspace.repo.owner, .name, .host | Repository parsed from the origin remote |
cost.total_cost_usd | Estimated session cost in USD, worked out locally at list price |
cost.total_duration_ms, cost.total_api_duration_ms | Wall-clock time, and time spent waiting on the API |
cost.total_lines_added, cost.total_lines_removed | Lines changed this session |
context_window.used_percentage, .remaining_percentage | Context used and left, already calculated |
context_window.context_window_size | 200,000, or 1,000,000 for extended-context models |
rate_limits.five_hour, rate_limits.seven_day | used_percentage and resets_at (Unix seconds), for Pro and Max subscribers |
session_id, session_name | Session ID, and the name from --name, /rename or the generated title |
effort.level, thinking.enabled, fast_mode | Reasoning effort, extended thinking, fast mode |
worktree.name, workspace.git_worktree | Worktree session name, or the linked git worktree you’re in |
pr.number, pr.url, pr.review_state | Open pull request (or GitLab merge request) for the branch |
vim.mode, output_style.name, agent.name | Vim mode, output style, and the agent name when you use --agent |
version, transcript_path | Claude Code version and the transcript file |
Not every field is always there, and your script has to cope:
- Missing until they apply:
session_name,workspace.repo,workspace.git_worktree,effort,vim,agent,pr,worktreeandrate_limits. Rate limits only appear for claude.ai Pro and Max subscribers, after the first API response, and each window can be missing on its own. nullearly on:context_window.used_percentageandremaining_percentagecan benullat the start of a session.current_usageisnullbefore the first API call and again right after/compact.
In jq, // 0 gives a default for null, and // empty prints nothing for a missing field.
Two numbers are often misread. used_percentage counts input tokens only (fresh input plus cache reads and writes), not output, and it comes from the last API response, so /context can show a higher number until the next one. total_cost_usd is an estimate made on your machine: it may not match your bill, and it resets to $0 when /clear starts a new session.
4. When it updates
The script runs when a session starts or resumes, then again when:
- a new assistant message arrives
/compactfinishes- the permission mode or vim mode changes
- you change the
commandin your settings - a
refreshIntervaltimer elapses, if you set one - a rate-limit window or a warm prompt cache reaches the reset or expiry time in the data the script last received
Updates are debounced by 300 ms, so a burst of changes runs the script once. If a new update arrives while the script is still running, Claude Code cancels the old run. Editing the script file doesn’t trigger a run; your change shows up on the next update.
Because nothing fires while the session sits idle, a clock, or a git status changed by something else, goes stale. Set "refreshInterval": 5 for that. The status line runs locally and doesn’t use any tokens. It hides while a permission prompt or the help menu is on screen.
5. Example scripts
Save each Bash example as ~/.claude/statusline.sh, make it executable with chmod +x ~/.claude/statusline.sh, and point command at it as in section 2. They need jq (brew install jq on macOS, sudo apt install jq on Debian and Ubuntu); example 5 needs only Python. Each one was tested against sample input, including the null values you get early in a session.
Example 1: model and folder
~/.claude/statusline.sh#!/bin/bash
input=$(cat)
model=$(jq -r '.model.display_name' <<< "$input")
dir=$(jq -r '.workspace.current_dir' <<< "$input")
echo "[$model] ${dir##*/}"
Prints [Opus] checkout-api. ${dir##*/} keeps only the last part of the path.
Example 2: git branch with a dirty marker
~/.claude/statusline.sh#!/bin/bash
input=$(cat)
model=$(jq -r '.model.display_name' <<< "$input")
dir=$(jq -r '.workspace.current_dir' <<< "$input")
export GIT_OPTIONAL_LOCKS=0
branch=$(git -C "$dir" branch --show-current 2>/dev/null)
dirty=""
if [ -n "$(git -C "$dir" status --porcelain --untracked-files=no 2>/dev/null)" ]; then
dirty="*"
fi
if [ -n "$branch" ]; then
echo "[$model] ${dir##*/} on $branch$dirty"
else
echo "[$model] ${dir##*/}"
fi
Prints [Opus] checkout-api on feature/login*, with * when tracked files have changes. GIT_OPTIONAL_LOCKS=0 stops git status from taking the index lock, so your status line doesn’t collide with a git command Claude is running. --untracked-files=no keeps it quick in big repositories. If it’s still slow, cache the result in a file named after session_id and refresh it every few seconds.
Example 3: context bar and cost, in color
~/.claude/statusline.sh#!/bin/bash
input=$(cat)
IFS=$'\t' read -r model pct cost < <(jq -r '[
.model.display_name,
(.context_window.used_percentage // 0 | floor),
(.cost.total_cost_usd // 0)
] | @tsv' <<< "$input")
green='\033[32m'; yellow='\033[33m'; red='\033[31m'; reset='\033[0m'
if [ "$pct" -ge 90 ]; then color=$red
elif [ "$pct" -ge 70 ]; then color=$yellow
else color=$green; fi
filled=$((pct / 10)); [ "$filled" -gt 10 ] && filled=10
bar=$(printf '%*s' "$filled" '' | tr ' ' '#')$(printf '%*s' $((10 - filled)) '' | tr ' ' '-')
cost=$(LC_NUMERIC=C printf '%.2f' "$cost")
printf '%b\n' "[$model] ${color}${bar} ${pct}%${reset} | \$${cost}"
Prints [Opus] #######--- 73% | $1.23, with the bar green under 70%, yellow from 70% and red from 90%. One jq call reads all three values, which matters because the script runs after every message. LC_NUMERIC=C stops locales that use a decimal comma from breaking printf. printf '%b' turns the \033 color codes into real escape characters.
Example 4: session name, worktree and rate limits
~/.claude/statusline.sh#!/bin/bash
jq -r '
def pct(w): if w then "\(w.used_percentage | floor)%" else empty end;
[
.model.display_name,
(.session_name // empty),
(.worktree.name // .workspace.git_worktree // empty | "worktree " + .),
("5h " + pct(.rate_limits.five_hour)),
("7d " + pct(.rate_limits.seven_day))
] | join(" | ")
'
Prints Opus | login fix | 5h 23% | 7d 41%. Anything missing, such as rate limits when you use an API key, or the worktree when you aren’t in one, is skipped instead of printing null. The whole script is one jq program reading stdin.
Example 5: Python, no jq
~/.claude/statusline.py#!/usr/bin/env python3
import json, os, subprocess, sys
data = json.load(sys.stdin)
model = data["model"]["display_name"]
cwd = data["workspace"]["current_dir"]
pct = int(data.get("context_window", {}).get("used_percentage") or 0)
try:
branch = subprocess.run(
["git", "-C", cwd, "branch", "--show-current"],
capture_output=True, text=True, timeout=2,
).stdout.strip()
except Exception:
branch = ""
parts = [f"[{model}]", os.path.basename(cwd)]
if branch:
parts.append(f"on {branch}")
parts.append(f"{pct}% context")
print(" ".join(parts))
Prints [Opus] checkout-api on feature/login 73% context. The two-second timeout keeps a slow git from holding up the status line. Set command to ~/.claude/statusline.py, or to python3 ~/.claude/statusline.py if you’d rather not make it executable.
6. Colors, multiple lines, links and width
- Multiple lines: each line your script prints becomes its own row.
- Colors: ANSI codes such as
\033[32m(green) and\033[0m(reset) work if your terminal supports them. Multi-line output with lots of escape codes is the most likely to render badly; switch to plain text if it does. - Links: OSC 8 sequences make text clickable (Cmd+click on macOS, Ctrl+click elsewhere) in terminals that support them, such as iTerm2, Kitty and WezTerm. Terminal.app doesn’t. Print them with
printf '%b'rather thanecho -e. - Width: Claude Code captures your script’s output, so
tput colscan’t see the terminal. Read$COLUMNSand$LINES, which Claude Code sets before each run. Keep it short anyway: Claude Code’s own notices share the row on the right and can cut a long line off.
7. Windows
On Windows, Claude Code runs the command through Git Bash when it’s installed, or PowerShell when it isn’t. Two rules avoid most problems:
- Use forward slashes in
command. Git Bash treats backslashes as escapes, soC:\Users\you\statusline.ps1arrives with its separators removed and fails with no error on screen.~works and means your Windows home folder. - Call PowerShell scripts through
powershell, which works whichever shell Claude Code picked:
settings.json{
"statusLine": {
"type": "command",
"command": "powershell -NoProfile -File C:/Users/you/.claude/statusline.ps1"
}
}
C:/Users/you/.claude/statusline.ps1$input_json = $input | Out-String | ConvertFrom-Json
$cwd = $input_json.cwd
$model = $input_json.model.display_name
$used = $input_json.context_window.used_percentage
$dirname = Split-Path $cwd -Leaf
if ($used) {
Write-Host "$dirname [$model] ctx: $used%"
} else {
Write-Host "$dirname [$model]"
}
With Git Bash installed, the Bash examples above work on Windows too, as long as jq is on your PATH. Example 5 works anywhere Python 3 does. In WSL, follow the Linux instructions.
8. Troubleshooting
Test the script on its own first. This sends it sample input the way Claude Code would:
Terminalecho '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/home/you/project"},"context_window":{"used_percentage":25},"session_id":"test"}' | ~/.claude/statusline.sh
- Blank status line. The script must exit with code 0 and print to stdout. Anything else, including output only on stderr, leaves it blank. Check it’s executable (
chmod +x) and that the#!line points at a real interpreter. - Blank in a new folder. The status line runs a shell command, so like hooks it waits for the workspace trust dialog.
claude --debuglogsStatus line command skipped: workspace trust not accepted. Restart and accept the dialog. - Blank after it used to work. Look for
"disableAllHooks": truein your settings: it turns off your status line too. At work,allowManagedHooksOnlyin managed settings allows only a status line defined in those managed settings. - See the error.
claude --debuglogs your script’s stderr on every run and its exit code on the first run in a session. nullor empty values. Some fields arenulluntil the first response. Use// 0or// emptyinjq, oror 0in Python.- Stale or laggy output. A slow script holds the status line until it exits, and gets cancelled when the next update starts. Cache slow commands, and set
refreshIntervalif you need updates while Claude is idle. - Color codes show up as text. Use
printf '%b'orecho -e; plainechoin Bash prints\033literally. - Inside tmux. Colors are limited to 256 and OSC 8 links may be stripped. See Using Claude Code in tmux.
Every session at once with Agent Pets
A status line describes the session in the terminal you’re looking at. When several sessions run at once, Agent Pets shows all of them while you’re in Chrome:
- Each Claude Code, Codex, OpenCode and omp session is a pet walking along the bottom of every page, with a capsule saying Thinking, Your Turn, Allow Bash? or Done.
- The toolbar badge counts the sessions waiting on you. Click a pet to answer Allow, Always or Deny, or to read its latest answer and reply.
npx getagentpets init adds hooks next to the ones you already have, and everything runs on your computer.
Get Agent Pets — was $25, now $6Get Agent Pets — $25
Questions
How do I add a status line to Claude Code?
Run /statusline followed by what you want to see, and Claude Code writes the script and the setting for you. Or add a statusLine object with "type": "command" and your script’s path to ~/.claude/settings.json.
How often does the Claude Code status line update?
When the session starts, after each new assistant message, after /compact, and when the permission mode or vim mode changes, debounced by 300 ms. Set refreshInterval to also run it every few seconds.
Can the status line show how much context is used?
Yes. Read context_window.used_percentage from the JSON on stdin. It counts input tokens from the last response and can be null at the start of a session, so give it a default.
Why is my Claude Code statusline not showing?
Usually the script failed: it must exit with code 0 and print to stdout. It also stays blank until you trust the folder, and when disableAllHooks is true. Run claude --debug to see the script’s errors.
How do I remove the Claude Code status line?
Run /statusline delete, or delete the statusLine key from your settings file.
Do I need jq for a Claude Code statusline?
No. jq is just a convenient way to read the JSON in a shell script. Any language that reads stdin and prints to stdout works; Example 5 above is a Python script that needs no jq.
Sources: Claude Code’s documentation on the status line, settings and environment variables; the git documentation on GIT_OPTIONAL_LOCKS.