Claude Code's default status bar shows two things: the current directory and the model name. That's it. But underneath, it supports a full bash script that receives a rich JSON payload on every update. Here's how I replaced the default with something actually useful.
How it works
Two changes are needed. First, point the status line to a script in ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
}
}
Second, create the script. Claude Code runs it after every API call and pipes a JSON object to stdin. Your script reads that JSON, formats whatever it wants, and prints it. Multi-line output works — each line becomes a row in the status bar.
chmod +x ~/.claude/statusline.sh
That's the entire mechanism.
What data is available
Here are the fields worth using:
| Field | What it gives you |
|---|---|
| workspace.current_dir | Full path to the working directory |
| workspace.git_worktree | Active worktree name, if any |
| session_name | Auto-generated name for the current session |
| model.display_name | Short model name — Sonnet 4.6, Opus 4.7, etc. |
| context_window.used_percentage | How much of the context window is consumed |
| context_window.total_input_tokens | Cumulative input tokens for the session |
| context_window.total_output_tokens | Cumulative output tokens for the session |
| context_window.current_usage.cache_read_input_tokens | Tokens served from cache (cheap) |
| context_window.current_usage.cache_creation_input_tokens | Tokens written to cache |
| cost.total_cost_usd | Total session cost in USD |
| cost.total_lines_added | Lines of code Claude wrote via Edit/Write tools |
| cost.total_lines_removed | Lines of code Claude deleted |
| rate_limits.five_hour.used_percentage | 4-hour rate limit consumption |
| rate_limits.five_hour.resets_at | Unix timestamp when the 4h window resets |
| rate_limits.seven_day.used_percentage | 7-day rate limit consumption |
| effort.level | Current effort level — low, normal, high |
| agent.name | Active sub-agent name, if one is running |
| vim.mode | Vim mode if vim keybindings are enabled |
Git branch is not included — you have to run git yourself inside the script.
Some fields are null early in a session before the first API call. Every jq expression should have a fallback: .field // "" or .field // 0.
Building it row by row
Row 1 — where you are
cwd=$(echo "$input" | jq -r '.workspace.current_dir // ""')
dir=$(basename "$cwd")
session=$(echo "$input" | jq -r '.session_name // ""')
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'; D='\033[2m'; Z='\033[0m'
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 "$session" ] && row1="${row1} ${D}[${session}]${Z}"
Directory and branch are joined — polymarket-bot(main) — instead of the git:(branch) style. The ✗ appears when the working tree is dirty.
Row 2 — session health
ctx=$(echo "$input" | jq -r '.context_window.used_percentage // "?"')
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}')
rl4h_resets_at=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // 0')
model=$(echo "$input" | jq -r '.model.display_name // ""')
effort=$(echo "$input" | jq -r '.effort.level // ""')
# Calculate time until rate limit resets
rl4h_resets_in=""
if [ "$rl4h_resets_at" != "0" ]; then
secs_left=$(( rl4h_resets_at - $(date +%s) ))
if [ "$secs_left" -gt 0 ]; then
mins_left=$(( secs_left / 60 ))
if [ "$mins_left" -ge 60 ]; then
rl4h_resets_in="$(( mins_left / 60 ))h$(( mins_left % 60 ))m"
else
rl4h_resets_in="${mins_left}m"
fi
fi
fi
M='\033[0;35m'
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
rl_str="4h:${rl4h}%"
[ -n "$rl4h_resets_in" ] && rl_str="${rl_str} ↻${rl4h_resets_in}"
row2="${row2} ${R}[${rl_str} | 7d:${rl7d}%]${Z}"
fi
[ -n "$model" ] && row2="${row2} ${D}${model}${Z}"
[ -n "$effort" ] && row2="${row2} ${D}| ${effort}${Z}"
Rate limits only appear when they're above zero, so the line stays clean during normal usage. The ↻13m indicator shows exactly how long until the window resets — useful when you're close to the limit and deciding whether to keep going.
Row 3 — cost and tokens
cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' | awk '{printf "%.3f", $1}')
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')
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}"
The token split is interesting in practice: most sessions look like in:68k | out:0.1k. Input is large because the full context is sent every request; output is small because responses are short. The cache read number shows how much of that input was served cheaply from cache — in a long session it's typically 95%+.
The final result

Complete script
#!/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}')
rl4h_resets_at=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // 0')
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)
rl4h_resets_in=""
if [ "$rl4h_resets_at" != "0" ]; then
secs_left=$(( rl4h_resets_at - $(date +%s) ))
if [ "$secs_left" -gt 0 ]; then
mins_left=$(( secs_left / 60 ))
if [ "$mins_left" -ge 60 ]; then
rl4h_resets_in="$(( mins_left / 60 ))h$(( mins_left % 60 ))m"
else
rl4h_resets_in="${mins_left}m"
fi
fi
fi
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
rl_str="4h:${rl4h}%"
[ -n "$rl4h_resets_in" ] && rl_str="${rl_str} ↻${rl4h_resets_in}"
row2="${row2} ${R}[${rl_str} | 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}"
What else you can add
The JSON has more fields that didn't make it into my setup but are worth knowing about:
-
cost.total_duration_ms— total session wall time. Format it as12m 34sfor a quick sense of how long you've been in the session. -
cost.total_lines_added/removedis already in row 3, but you could also compute a ratio to see how much net code was generated vs deleted. -
vim.mode— if you use Claude Code's vim keybindings,NORMAL/INSERTis available. -
agent.name— already included in row 2, shows only when a sub-agent is active. -
thinking.enabled— shows whether extended thinking is on for the current request. -
worktree.name— row 1 already handles this, shows a⎇indicator when you're in a worktree.
The script runs on every status bar refresh, so keep it fast. The git calls are the slowest part — on a large repo with many files, the diff --quiet check for dirty state adds a few milliseconds. Worth it for the ✗ indicator, but something to keep in mind.