>_ resources
Claude Code Statusline
A real-time analytics dashboard that lives in your Claude Code terminal. Tracks session cost, token burn rate, context window usage, cache efficiency, code changes, git status, API usage limits, and productivity — all in a single line.
~280 lines of Bash · Zero dependencies beyond jq and bc
>_Installation
1. Prerequisites
The script requires jq (JSON parsing) and bc (math). Both are pre-installed on macOS. On Linux:
# Debian/Ubuntu
sudo apt install jq bc
# Arch
sudo pacman -S jq bc2. Download the script
Save the script to your Claude config directory:
curl -fsSL https://raba.pl/statusline-command.sh \
-o ~/.claude/statusline-command.sh3. Configure Claude Code
Add the statusline configuration to your ~/.claude/settings.json. Merge this into your existing settings:
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline-command.sh"
}
}4. Restart Claude Code
Close and reopen Claude Code. The statusline will appear at the bottom of your terminal. That's it.
>_Sections Breakdown
The statusline is composed of segments separated by dim vertical bars. Each segment shows a different metric. Here is what each one means:
Directory
rabaplShows the name of your current working directory so you always know which project Claude is operating in.
Model
Opus 4.6Displays the active Claude model. Helps confirm whether you're running Opus, Sonnet, or Haiku.
Context Bar
████████████░░░░░░░░ 62%A 20-character visual progress bar showing context window usage. Colors shift from green (0-35%) through yellow (35-70%) to red (70-100%) as the window fills up.
Tokens
↑142.5K ↓ 18.3KTotal input (↑) and output (↓) tokens for the session. Values are formatted as K (thousands) or M (millions) for readability.
Session Cost
$2.85 $0.42/mCurrent session cost in USD (bold). When the session exceeds 60 seconds, a cost-per-minute rate is appended.
Daily Cost
today:$14.23Cumulative spending across all sessions for the current day. Costs are logged to ~/.claude/statusline-cost-log and deduplicated by session ID before summing.
Burn Rate & ETA
8420t/m ~23mToken burn rate (tokens per minute) and estimated time until the context window is exhausted at the current pace. Shows hours for long sessions.
Code Changes
+847/-129Lines of code added (green) and removed (red) during the session. A quick indicator of how much code is being generated.
Cache Hit Rate
73%Prompt cache hit rate — the percentage of input tokens served from cache vs. freshly processed. Higher values mean lower cost and faster responses.
Productivity
24810tok/$Output tokens per dollar spent. A measure of how efficiently the model converts your spend into generated output.
Rank
🏆 / 🥇 / 🥈 / 🥉Productivity tier badge. 🏆 = 50K+ tok/$, 🥇 = 20K+, 🥈 = 10K+, 🥉 = 5K+. Gamifies your cost efficiency.
Git Status
main S:2 U:1 A:0Current branch name with file counts — S (staged, green), U (unstaged, yellow), A (untracked/added, red). Branch name turns yellow when dirty. Git data is cached for 5 seconds.
Clock
16:42Current time of day. Simple but useful during long sessions to avoid losing track of time.
Time Remaining
~23mEstimated time remaining before the context window fills up, based on the current token burn rate.
Usage Limits (Pro/Max)
5h:37% 7d:26%Real-time usage limits from the Anthropic API. Shows 5-hour and 7-day rolling window utilization percentages. Requires macOS Keychain credentials (Pro/Max plans only). Cached for 60 seconds.
>_Usage Limits (Pro/Max)
Claude Code Pro and Max plans have rolling usage windows. The script can fetch your real-time utilization from the Anthropic API and display it directly in the statusline — no more running /usage manually.
API Endpoint
The script calls the undocumented /api/oauth/usage endpoint on api.anthropic.com. It returns JSON with rolling window data:
{
"five_hour": {
"utilization": 37.0,
"resets_at": "2026-02-24T04:59:59.000000+00:00"
},
"seven_day": {
"utilization": 26.0,
"resets_at": "2026-02-28T14:59:59.771647+00:00"
},
"seven_day_opus": { "utilization": 12.0, "resets_at": "..." },
"seven_day_sonnet": { "utilization": 1.0, "resets_at": "..." },
"extra_usage": {
"is_enabled": false,
"monthly_limit": null,
"used_credits": null,
"utilization": null
}
}Authentication
The script extracts your OAuth token from the macOS Keychain using security find-generic-password -s "Claude Code-credentials" -w. The token is a JSON blob containing an access token which is used as a Bearer token in the API call. This only works on macOS with Pro/Max plans — API key users don't have rolling limits.
Caching & Rate Limiting
The limits response is cached to /tmp/claude-statusline-limits-cache for 60 seconds. This avoids hammering the API on every statusline render. If the API call fails (network error, expired token, Linux), the limits segment is silently omitted.
What's Displayed
The statusline shows two values:
5h:37%— 5-hour rolling window utilization. This is the short-term rate limit that resets most frequently.7d:26%— 7-day rolling window utilization. The longer-term cap on total usage.
Colors shift from green (<50%) through yellow (50-80%) to red (80%+) to warn you before hitting limits.
>_How It Works
Data Flow
Claude Code pipes a JSON object to the statusline command via stdin on every render cycle. The JSON includes the active model, context window metrics, token counts, cost data, code change stats, session ID, and workspace info. The script parses this with jq, computes derived metrics with bc, and echoes a formatted string with ANSI color codes.
Performance
The script runs frequently, so performance matters. Git operations (branch, diff, ls-files) are cached for 5 seconds in /tmp/claude-statusline-git-cache. Daily cost totals are cached for 30 seconds. Token counts use K/M formatting to avoid long numbers.
Cost Logging
Every render appends a line to ~/.claude/statusline-cost-log in the format YYYY-MM-DD|session_id|cost_usd. When computing the daily total, the script deduplicates by session_id (keeping the latest cost per session) before summing. This gives you accurate cross-session daily spend tracking.
Color Gradient
The context window progress bar uses a 20-character block display. The first 7 blocks are green (safe), blocks 8-14 are yellow (caution), and blocks 15-20 are red (critical). Empty positions use a dim shade character.
New in 2026: Additional JSON Fields
Claude Code now exposes several additional fields in the statusline JSON that you can use in your scripts:
exceeds_200k_tokens— boolean, true when total tokens exceed 200K thresholdtranscript_path— path to the conversation transcript fileoutput_style.name— current output style namevim.mode— NORMAL or INSERT when vim mode is enabledagent.name— agent name when running with --agent flag
Multi-line output and OSC 8 clickable links are also now officially supported. Each echo statement produces a separate row, and you can use the padding setting to add horizontal spacing.
>_Full Script
The complete statusline-command.sh — ~280 lines of Bash. You can also download it directly.
#!/usr/bin/env bash
input=$(cat)
# --- Extract fields (correct paths from official docs) ---
model_name=$(echo "$input" | jq -r '.model.display_name // "?"')
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "~"')
project_dir=$(echo "$input" | jq -r '.workspace.project_dir // ""')
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0')
remaining_pct=$(echo "$input" | jq -r '.context_window.remaining_percentage // 100')
total_in=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
total_out=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
cur_in=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
cur_out=$(echo "$input" | jq -r '.context_window.current_usage.output_tokens // 0')
cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
context_size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
api_ms=$(echo "$input" | jq -r '.cost.total_api_duration_ms // 0')
lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
version=$(echo "$input" | jq -r '.version // ""')
session_id=$(echo "$input" | jq -r '.session_id // ""')
# --- Colors ---
red=$'\033[31m'; grn=$'\033[32m'; ylw=$'\033[33m'; blu=$'\033[34m'
mag=$'\033[35m'; cyn=$'\033[36m'; dim=$'\033[90m'; bld=$'\033[1m'; rst=$'\033[0m'; wht=$'\033[97m'
sep=" ${dim}${rst} "
# --- Format tokens ---
fmt() {
local n=$1
if [ "$n" -ge 1000000 ] 2>/dev/null; then echo "$(echo "scale=1; $n/1000000" | bc -l)M"
elif [ "$n" -ge 1000 ] 2>/dev/null; then echo "$(echo "scale=1; $n/1000" | bc -l)K"
else echo "$n"; fi
}
# --- Gradient progress bar (20 chars) ---
filled=$(printf "%.0f" "$(echo "$used_pct * 0.2" | bc -l 2>/dev/null || echo 0)")
bar=""
for ((i=0; i<filled && i<20; i++)); do
if [ $i -lt 7 ]; then bar+=$'\033[32m'"█"
elif [ $i -lt 14 ]; then bar+=$'\033[33m'"█"
else bar+=$'\033[31m'"█"; fi
done
for ((i=filled; i<20; i++)); do bar+="${dim}░"; done
bar+="${rst}"
# --- Cost ---
if [ "$(echo "$cost_usd < 0.01" | bc -l 2>/dev/null)" = "1" ]; then cost_str='<$0.01'
elif [ "$(echo "$cost_usd < 1" | bc -l 2>/dev/null)" = "1" ]; then cost_str=$(printf '$%.3f' "$cost_usd")
else cost_str=$(printf '$%.2f' "$cost_usd"); fi
# --- Duration ---
total_secs=$((duration_ms / 1000))
api_secs=$((api_ms / 1000))
h=$((total_secs / 3600)); m=$(((total_secs % 3600) / 60)); s=$((total_secs % 60))
if [ $h -gt 0 ]; then dur="${h}h${m}m"
elif [ $m -gt 0 ]; then dur="${m}m${s}s"
else dur="${s}s"; fi
# --- Burn rate & ETA ---
burn_str=""
if [ $total_secs -gt 0 ]; then
tpm=$(echo "scale=0; ($total_in + $total_out) * 60 / $total_secs" | bc -l 2>/dev/null || echo "0")
if [ "$tpm" -gt 0 ] 2>/dev/null; then
used_tokens=$((cur_in + cache_create + cache_read))
remaining=$((context_size - used_tokens))
if [ "$remaining" -gt 0 ] 2>/dev/null; then
ml=$(echo "scale=0; $remaining / $tpm" | bc -l 2>/dev/null || echo "?")
if [ "$ml" -lt 60 ] 2>/dev/null; then eta="~${ml}m"
else eta="~$((ml / 60))h"; fi
else
eta="full!"
fi
burn_str="${tpm}t/m ${eta}"
fi
fi
# --- Cost per minute ---
cpm_str=""
if [ $total_secs -gt 60 ]; then
cpm=$(echo "scale=3; $cost_usd * 60 / $total_secs" | bc -l 2>/dev/null || echo "0")
cpm_str="$(printf '$%.2f/m' "$cpm")"
fi
# --- Cache hit rate ---
cache_str=""
total_cache=$((cache_create + cache_read))
if [ "$total_cache" -gt 0 ] 2>/dev/null; then
hit_pct=$(echo "scale=0; $cache_read * 100 / ($cache_read + $cache_create + $cur_in)" | bc -l 2>/dev/null || echo "0")
cache_str="${hit_pct}%"
fi
# --- Lines changed ---
lines_str=""
if [ "$lines_added" -gt 0 ] 2>/dev/null || [ "$lines_removed" -gt 0 ] 2>/dev/null; then
lines_str="${grn}+${lines_added}${rst}/${red}-${lines_removed}${rst}"
fi
# --- Git (with caching for perf) ---
CACHE_FILE="/tmp/claude-statusline-git-cache"
CACHE_MAX_AGE=5
cd "$cwd" 2>/dev/null || cd ~
cache_stale() {
[ ! -f "$CACHE_FILE" ] || \
[ $(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0))) -gt $CACHE_MAX_AGE ]
}
if cache_stale; then
if git rev-parse --git-dir > /dev/null 2>&1; then
branch=$(git branch --show-current 2>/dev/null || echo "detached")
staged=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
unstaged=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
echo "${branch}|${staged}|${unstaged}|${untracked}" > "$CACHE_FILE"
else
echo "|||" > "$CACHE_FILE"
fi
fi
IFS='|' read -r branch staged unstaged untracked < "$CACHE_FILE"
git_str=""
if [ -n "$branch" ]; then
dirty=""
[ "$staged" -gt 0 ] 2>/dev/null && dirty+=" ${grn}S:${staged}${rst}"
[ "$unstaged" -gt 0 ] 2>/dev/null && dirty+=" ${ylw}U:${unstaged}${rst}"
[ "$untracked" -gt 0 ] 2>/dev/null && dirty+=" ${red}A:${untracked}${rst}"
if [ -n "$dirty" ]; then
git_str="${ylw}${branch}${rst}${dirty}"
else
git_str="${grn}${branch}${rst}"
fi
fi
# --- Daily cumulative cost (cached, 30s refresh) ---
COST_LOG="$HOME/.claude/statusline-cost-log"
COST_CACHE="/tmp/claude-statusline-cost-totals"
COST_CACHE_AGE=30
today=$(date +%Y-%m-%d)
echo "${today}|${session_id}|${cost_usd}" >> "$COST_LOG" 2>/dev/null
cost_totals_stale() {
[ ! -f "$COST_CACHE" ] || \
[ $(($(date +%s) - $(stat -f %m "$COST_CACHE" 2>/dev/null || stat -c %Y "$COST_CACHE" 2>/dev/null || echo 0))) -gt $COST_CACHE_AGE ]
}
daily_cost=""
if cost_totals_stale && [ -f "$COST_LOG" ]; then
d_cost=$(awk -F'|' -v d="$today" '$1==d {a[$2]=$3} END {s=0; for(k in a) s+=a[k]; printf "%.2f",s}' "$COST_LOG" 2>/dev/null || echo "0")
echo "${d_cost}" > "$COST_CACHE"
fi
if [ -f "$COST_CACHE" ]; then
read -r daily_cost < "$COST_CACHE"
fi
cost_history_str=""
if [ -n "$daily_cost" ] && [ "$daily_cost" != "0.00" ] 2>/dev/null; then
cost_history_str="today:\$${daily_cost}"
fi
# --- MCP server count ---
mcp_str=""
mcp_count=0
if [ -f "$HOME/.claude/settings.json" ]; then
plugin_count=$(jq '[.enabledPlugins // {} | to_entries[] | select(.value == true)] | length' "$HOME/.claude/settings.json" 2>/dev/null || echo "0")
server_count=$(jq '.mcpServers // {} | keys | length' "$HOME/.claude/settings.json" 2>/dev/null || echo "0")
mcp_count=$((plugin_count + server_count))
fi
[ "$mcp_count" -gt 0 ] 2>/dev/null && mcp_str="${mcp_count}"
# --- Productivity score & rank ---
prod_str=""
rank_str=""
if [ "$(echo "$cost_usd > 0.01" | bc -l 2>/dev/null)" = "1" ]; then
prod=$(echo "scale=0; $total_out / $cost_usd" | bc -l 2>/dev/null || echo "0")
prod_str="${prod}tok/\$"
if [ "$prod" -ge 50000 ] 2>/dev/null; then rank_str="🏆"
elif [ "$prod" -ge 20000 ] 2>/dev/null; then rank_str="🥇"
elif [ "$prod" -ge 10000 ] 2>/dev/null; then rank_str="🥈"
elif [ "$prod" -ge 5000 ] 2>/dev/null; then rank_str="🥉"
else rank_str=""; fi
fi
# --- Usage limits (Pro/Max only, macOS Keychain, cached 60s) ---
LIMITS_CACHE="/tmp/claude-statusline-limits-cache"
LIMITS_CACHE_AGE=60
limits_str=""
limits_stale() {
[ ! -f "$LIMITS_CACHE" ] || \
[ $(($(date +%s) - $(stat -f %m "$LIMITS_CACHE" 2>/dev/null || stat -c %Y "$LIMITS_CACHE" 2>/dev/null || echo 0))) -gt $LIMITS_CACHE_AGE ]
}
if limits_stale; then
cred_json=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || echo "")
if [ -n "$cred_json" ]; then
access_token=$(echo "$cred_json" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
if [ -n "$access_token" ]; then
usage_json=$(curl -sf --max-time 5 -H "Authorization: Bearer ${access_token}" \
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null || echo "")
if [ -n "$usage_json" ]; then
five_h=$(echo "$usage_json" | jq -r '.five_hour.utilization // empty' 2>/dev/null)
seven_d=$(echo "$usage_json" | jq -r '.seven_day.utilization // empty' 2>/dev/null)
echo "${five_h}|${seven_d}" > "$LIMITS_CACHE"
fi
fi
fi
fi
if [ -f "$LIMITS_CACHE" ]; then
IFS='|' read -r five_h seven_d < "$LIMITS_CACHE"
if [ -n "$five_h" ] && [ -n "$seven_d" ]; then
lim_color="${grn}"
[ "$(echo "$five_h > 50" | bc -l 2>/dev/null)" = "1" ] && lim_color="${ylw}"
[ "$(echo "$five_h > 80" | bc -l 2>/dev/null)" = "1" ] && lim_color="${red}"
five_h_int=$(printf "%.0f" "$five_h" 2>/dev/null || echo "$five_h")
seven_d_int=$(printf "%.0f" "$seven_d" 2>/dev/null || echo "$seven_d")
limits_str="${lim_color}5h:${five_h_int}%${rst} ${dim}7d:${seven_d_int}%${rst}"
fi
fi
# --- Build output ---
dir_name="${cwd##*/}"
parts=""
parts+="📁 ${wht}${dir_name}${rst}"
parts+="${sep}${cyn}🤖 ${model_name}${rst}"
parts+="${sep}${bar} $(printf '%.0f' "$used_pct")%"
parts+="${sep}↑$(fmt $total_in) ↓$(fmt $total_out)"
parts+="${sep}💰 ${bld}${cost_str}${rst}"
[ -n "$cpm_str" ] && parts+=" ${dim}${cpm_str}${rst}"
[ -n "$cost_history_str" ] && parts+="${sep}📊 ${dim}${cost_history_str}${rst}"
[ -n "$burn_str" ] && parts+="${sep}🔥 ${dim}${burn_str}${rst}"
[ -n "$lines_str" ] && parts+="${sep}✏️ ${lines_str}"
[ -n "$cache_str" ] && parts+="${sep}💾 ${dim}${cache_str}${rst}"
[ -n "$prod_str" ] && parts+="${sep}⚡ ${dim}${prod_str}${rst}"
[ -n "$rank_str" ] && parts+=" ${rank_str}"
[ -n "$git_str" ] && parts+="${sep}🌿 ${git_str}"
parts+="${sep}🕐 ${dim}$(date '+%H:%M')${rst}"
if [ -n "$eta" ]; then
parts+="${sep}⏳ ${dim}${eta}${rst}"
fi
[ -n "$limits_str" ] && parts+="${sep}📡 ${limits_str}"
echo "$parts"