The default Claude Code status line shows almost nothing useful — just the current directory and model name. But it supports a full bash script that receives a rich JSON payload every time the status bar refreshes. Here's how to build something worth looking at.
What you can actually show
Claude Code pipes a JSON object to your script via stdin after every API call. The full schema:
{
"workspace": {
"current_dir": "/Users/you/project",
"git_worktree": "feature-xyz"
},
"session_name": "Refactor authentication flow",
"model": { "display_name": "Sonnet 4.6" },
"context_window": {
"used_percentage": 27,
"total_input_tokens": 53800,
"total_output_tokens": 200,
"current_usage": {
"cache_read_input_tokens": 52300,
"cache_creation_input_tokens": 1500
}
},
"cost": {
"total_cost_usd": 2.268,
"total_lines_added": 175,
"total_lines_removed": 56
},
"rate_limits": {
"five_hour": { "used_percentage": 11.0 },
"seven_day": { "used_percentage": 23.0 }
},
"effort": { "level": "high" },
"agent": { "name": "security-reviewer" },
"vim": { "mode": "NORMAL" }
}
Git branch is not included — you have to run git yourself inside the script.
Wiring it up
In ~/.claude/settings.json, replace the default statusLine with:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
}
}
Then create ~/.claude/statusline.sh and make it executable:
chmod +x ~/.claude/statusline.sh
The script reads the JSON from stdin, formats whatever you want, and prints it. Multi-line output works — each \n becomes a new row in the status bar.
The script
This is the 3-row version I landed on:
#!/usr/bin/env bash
input=$(cat)
cwd=$(echo "$input" | jq -r '.workspace.current_dir // ""')
dir=$(basename "$cwd")
worktree=$(echo "$input" | jq -r '.workspace.git_worktree // ""')
session=$(echo "$input" | jq -r '.session_name // ""')
model=$(echo "$input" | jq -r '.model.display_name // ""')
ctx=$(echo "$input" | jq -r '.context_window.used_percentage // "?"')
cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' | awk '{printf "%.3f", $1}')
rl4h=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // 0' | awk '{printf "%.1f", $1}')
rl7d=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // 0' | awk '{printf "%.1f", $1}')
effort=$(echo "$input" | jq -r '.effort.level // ""')
agent=$(echo "$input" | jq -r '.agent.name // ""')
tok_in=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0' | awk '{printf "%.1fk", $1/1000}')
tok_out=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0' | awk '{printf "%.1fk", $1/1000}')
tok_total=$(echo "$input" | jq -r '(.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0)' | awk '{printf "%.1fk", $1/1000}')
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0' | awk '{printf "%.1fk", $1/1000}')
cache_write=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0' | awk '{printf "%.1fk", $1/1000}')
lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null || \
git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
G='\033[1;32m'; C='\033[0;36m'; R='\033[0;31m'
Y='\033[0;33m'; M='\033[0;35m'; D='\033[2m'; Z='\033[0m'
# Row 1: location
if [ -n "$branch" ]; then
if ! git -C "$cwd" --no-optional-locks diff --quiet 2>/dev/null || \
! git -C "$cwd" --no-optional-locks diff --cached --quiet 2>/dev/null; then
row1="${G}➜${Z} ${C}${dir}(${R}${branch}${C}) ${Y}✗${Z}"
else
row1="${G}➜${Z} ${C}${dir}(${R}${branch}${C})${Z}"
fi
else
row1="${G}➜${Z} ${C}${dir}${Z}"
fi
[ -n "$worktree" ] && row1="${row1} ${Y}⎇ ${worktree}${Z}"
[ -n "$session" ] && row1="${row1} ${D}[${session}]${Z}"
# Row 2: context + rate limits + model + effort
indent=" "
row2="${M}ctx:${ctx}%${Z}"
rl4h_gt=$(echo "$rl4h" | awk '{print ($1 > 0) ? "1" : "0"}')
rl7d_gt=$(echo "$rl7d" | awk '{print ($1 > 0) ? "1" : "0"}')
if [ "$rl4h_gt" = "1" ] || [ "$rl7d_gt" = "1" ]; then
row2="${row2} ${R}[4h:${rl4h}% | 7d:${rl7d}%]${Z}"
fi
[ -n "$agent" ] && row2="${row2} ${C}[${agent}]${Z}"
[ -n "$model" ] && row2="${row2} ${D}${model}${Z}"
[ -n "$effort" ] && row2="${row2} ${D}| ${effort}${Z}"
# Row 3: cost + tokens + cache + lines changed
row3="${Y}\$${cost}${Z}"
row3="${row3} ${D}${tok_total} tok (in:${tok_in} | out:${tok_out})${Z}"
row3="${row3} ${D}cache(read:${cache_read} | write:${cache_write})${Z}"
row3="${row3} ${G}+${lines_added}${Z}/${R}-${lines_removed}${Z}"
printf '%b\n' "$row1"
printf '%b\n' "${indent}${row2}"
printf '%b\n' "${indent}${row3}"
Result

Row 1 — where you are: directory, git branch (red when dirty), session name.
Row 2 — session health: context window %, rate limits (only shown when > 0), active model, effort level.
Row 3 — cost tracking: total spend, token breakdown (input vs output), cache hits vs writes, lines of code added/removed.
A few things worth knowing
context_window.total_input_tokens is cumulative for the session, not per-request. Same for cost. The numbers reset when you start a new session.
Cache hits are cheap. In most sessions you'll see in:53k with cache_read:52k — that means 97% of input tokens hit the cache and were billed at ~10x less than fresh tokens. The cost would be much higher without it.
cost.total_lines_added/removed tracks what Claude wrote via Edit/Write tools, not what you typed. It's a rough proxy for how much code was actually generated.
Rate limits only appear when > 0, so the status bar stays clean during light usage. The five_hour field in the JSON is the 4-hour window — the naming is a quirk of the API.
null fields are common early in a session before the first API call completes. The // 0 and // "" fallbacks in every jq expression handle this.