Claude Code Statusline: Setup, JSON Fields and 6 Tested Scripts
Context at 72%, $1.87 spent, branch feature/login: a Claude Code status line keeps numbers like these on a row under the prompt while you work. Claude Code pipes session data into your script as JSON on stdin and shows whatever it prints. The script runs locally and doesn't consume API tokens.
Turn it on: /statusline or settings.json
The quick route is the /statusline command. Describe what you want, and Claude Code writes a script into ~/.claude/ and updates your settings; approve the file edits when it asks. Name each field, and say what to show when it's missing:
/statusline Two lines. Line 1: the model display name in brackets, the current folder name, and the git branch with * when there are uncommitted changes. Line 2: a 10-cell bar of context_window.used_percentage, green under 60%, yellow from 60%, red from 80%, then the session cost as $0.00 and the elapsed minutes. Treat any missing or null field as 0. Write it in bash with jq.To take it off again, run /statusline delete or remove the statusLine field from your settings.
The manual route is the same block written by hand, in your user settings or in project settings if the whole repo should get it:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0,
"refreshInterval": 5
}
}| Field | What it does |
|---|---|
type | Always "command" |
command | A script path or an inline shell command; it runs in a shell |
padding | Extra horizontal spacing in characters, default 0 |
refreshInterval | Also re-runs the command every N seconds, minimum 1. Leave it out to run on events only |
hideVimModeIndicator | true hides the built-in -- INSERT -- text when your script shows vim.mode itself |
Every field after command is optional, and Claude Code picks up the change as soon as you save the file. A separate subagentStatusLine setting rewrites the rows of the agent panel, one per subagent.
Start with /statusline, then edit the file it wrote. It's a plain script.
When Claude Code re-runs the script
The script runs once when a session starts or resumes, then again on events: a new assistant message, /compact finishing, a permission-mode change, a vim-mode toggle, or an edit to command. A refreshInterval timer adds runs on top. So does a rate-limit window, or a warm prompt cache, reaching the reset or expiry time in the data your script last saw.
Updates are debounced at 300 ms, and a new update cancels a script that's still running. Output shows only after the script exits with code 0, so a non-zero exit or empty stdout leaves the row blank. The row also hides during the help menu and permission prompts.
That shapes the script. Anything that changes while Claude sits idle, a clock for instance, needs refreshInterval, because no event will fire. Anything slow delays the row: a git status in a big repo belongs behind a cache, and Anthropic's caching example keys a temp file on session_id. For width, read $COLUMNS. Claude Code captures your output, so tput cols can't see the terminal.
The JSON your script receives
Each run gets one JSON object. These are the fields worth putting on a status line; the full field list adds prompt-cache statistics, the transcript path, the output style and a few more.
| Field | Holds | Absent when |
|---|---|---|
model.display_name, model.id | Model name and identifier | Never |
workspace.current_dir | Working directory. Same value as cwd; this one is preferred | Never |
workspace.project_dir | Where Claude Code was launched | Never |
workspace.git_worktree | Name of the linked git worktree you're in | In the main working tree |
workspace.repo.owner, .name, .host | Repo identity from the origin remote | Outside a repo, or with no origin |
context_window.used_percentage | Share of the context window in use | Never, but it can be null early in a session |
context_window.context_window_size | Max tokens: 200000 by default, 1000000 with extended context | Never |
cost.total_cost_usd | Estimated session cost in USD | Never; resets to $0 on /clear |
cost.total_duration_ms, cost.total_api_duration_ms | Session wall-clock time, and time spent waiting on the API | Never |
cost.total_lines_added, cost.total_lines_removed | Lines of code changed | Never |
rate_limits.five_hour.*, rate_limits.seven_day.* | used_percentage (0 to 100) and resets_at (epoch seconds) | Off Pro and Max plans, and before the first API response |
effort.level | low, medium, high, xhigh or max | The model doesn't support effort |
session_name | The --name or /rename name, else the AI-generated title | Neither exists yet |
pr.number, pr.url, pr.review_state | Open PR (or GitLab merge request) for the branch | No open PR found; gone once it merges or closes |
worktree.name, worktree.branch | The Claude Code worktree session you're in | Outside a worktree session |
vim.mode | NORMAL, INSERT, VISUAL or VISUAL LINE | Vim mode is off |
agent.name | Agent set with --agent or agent settings | No agent configured |
version | Claude Code version | Never |
The cost figure is computed client-side at list price unless a modelPricing table is in effect, so it can differ from your bill. Rate limits also appear behind a Claude apps gateway that sets a spend limit, under rate_limits.spend_limit.
used_percentage is calculated from input tokens only: fresh input plus cache writes plus cache reads, no output tokens. And /context can read higher than the status line, because it estimates the messages added since the last response. Read used_percentage and don't recompute it.
Save this as sample.json. It holds only documented fields, with values chosen so every script below has something to show:
{
"session_id": "test-session-abc",
"model": { "id": "claude-opus-5-5", "display_name": "Opus" },
"workspace": {
"current_dir": "/home/you/my-app",
"project_dir": "/home/you/my-app",
"git_worktree": "feature-xyz"
},
"cost": {
"total_cost_usd": 1.8734,
"total_duration_ms": 1384000,
"total_api_duration_ms": 412000,
"total_lines_added": 156,
"total_lines_removed": 23
},
"context_window": {
"context_window_size": 200000,
"used_percentage": 72,
"remaining_percentage": 28
},
"rate_limits": {
"five_hour": { "used_percentage": 23.5, "resets_at": 1738425600 },
"seven_day": { "used_percentage": 41.2, "resets_at": 1738857600 }
},
"pr": { "number": 1234, "url": "https://github.com/anthropics/claude-code/pull/1234", "review_state": "pending" }
}Test a script with ./statusline.sh < sample.json, then delete fields and run it again. A fresh session sends a thinner object than this one, and the script has to survive that.
Six status line scripts to copy
Each script reads stdin, prints one or two lines and exits 0. Save it under ~/.claude/, run chmod +x on it, and put its path in command. The output under each one is what it printed for sample.json on macOS's stock bash 3.2 with jq 1.7.1 (Node 26 for the last), with current_dir pointed at a git repo on branch feature/login that had uncommitted changes. The first five need jq installed.
1. Model, folder and git branch
git -C points git at the folder from the JSON, and a trailing * flags uncommitted changes.
#!/bin/bash
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
# Branch of the folder Claude is working in; * when there are uncommitted changes
BRANCH=$(git -C "$DIR" branch --show-current 2>/dev/null)
if [ -n "$BRANCH" ]; then
[ -n "$(git -C "$DIR" status --porcelain 2>/dev/null)" ] && BRANCH="$BRANCH*"
echo "[$MODEL] ${DIR##*/} | $BRANCH"
else
echo "[$MODEL] ${DIR##*/}"
fiOutput:
[Opus] my-app | feature/login*Outside a repo it prints [Opus] my-app. On a large repo, put the two git calls behind the cache.
2. Context bar that changes color
A 10-cell bar of used_percentage: green below 60%, yellow from 60%, red from 80%. One jq call returns both values as tab-separated text, which saves starting a second jq process on every update.
#!/bin/bash
input=$(cat)
# One jq call, two tab-separated values
IFS=