Skip to content

Instantly share code, notes, and snippets.

@chasebolt
Created March 26, 2026 20:40
Show Gist options
  • Select an option

  • Save chasebolt/d3269b3fad36a3c66a041ffc63edd537 to your computer and use it in GitHub Desktop.

Select an option

Save chasebolt/d3269b3fad36a3c66a041ffc63edd537 to your computer and use it in GitHub Desktop.
Claude Code Custom Statusline — rich multi-line status with usage bars, git branch, and context window tracking

Claude Code Custom Statusline

A rich, multi-line statusline for Claude Code that shows model info, context window usage, rate limit utilization, git branch status, and more — all with colored progress bars.

Preview

Claude Opus 4.6 | 12% used | 120k / 1.0m | thinking: Off
project: my-app   | current: ●●○○○○○○○○ 15% | weekly: ●○○○○○○○○○ 8%  | extra: ○○○○○○○○○○ $0.00/$100.00
branch: feat/login | resets 3:30pm             | resets mar 28, 3:30pm   | resets apr 1

What it shows

Line 1: Model name | context window % used | tokens used/total | thinking mode on/off

Line 2: Project folder | 5-hour (current) usage bar | 7-day (weekly) usage bar | extra usage spend

Line 3: Git branch (yellow if dirty, green if clean) | reset times for each usage tier

Features

  • Color-coded progress bars (green -> orange -> yellow -> red as usage increases)
  • Git branch with dirty/clean state detection
  • Git worktree awareness
  • Non-blocking API calls — usage data is fetched in the background and cached
  • Cross-platform OAuth token resolution (macOS Keychain, Linux credential files, GNOME Keyring)
  • Works on macOS and Linux

Requirements

  • Claude Code CLI
  • jq — for JSON parsing
  • curl — for API calls
  • git — for branch detection (optional)

Installation

1. Download the script

mkdir -p ~/.claude
curl -o ~/.claude/statusline.sh \
  'https://gist.githubusercontent.com/chasebolt/d3269b3fad36a3c66a041ffc63edd537/raw/statusline.sh'
chmod +x ~/.claude/statusline.sh

Or manually copy statusline.sh to ~/.claude/statusline.sh.

2. Configure Claude Code

Add the statusline to your Claude Code settings (~/.claude/settings.json):

# If you don't have a settings file yet, create one:
cat > ~/.claude/settings.json << 'EOF'
{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline.sh"
  }
}
EOF

If you already have a settings.json, just add the statusLine block:

{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline.sh"
  }
}

3. Restart Claude Code

Close and reopen Claude Code. The statusline should appear at the bottom of the terminal.

How it works

  1. Claude Code pipes JSON context (model info, token counts, cwd) to the script via stdin
  2. The script parses this with jq and formats line 1 (model + context window)
  3. For usage data (rate limits), it reads from a local cache (/tmp/claude/statusline-usage-cache.json)
  4. If the cache is stale (>60s), it kicks off a background curl to the Anthropic OAuth usage API — the statusline never blocks waiting for a network response
  5. OAuth tokens are resolved automatically from your system's credential store

Customization

Colors

Edit the ANSI color variables near the top of the script:

blue='\033[38;2;0;153;255m'
orange='\033[38;2;255;176;85m'
green='\033[38;2;0;160;0m'
cyan='\033[38;2;46;149;153m'
red='\033[38;2;255;85;85m'
yellow='\033[38;2;230;200;0m'
white='\033[38;2;220;220;220m'

Progress bar style

Change the filled/empty characters in the build_bar() function:

for ((i=0; i<filled; i++)); do filled_str+=""; done   # filled dots
for ((i=0; i<empty; i++)); do empty_str+=""; done     # empty dots

Cache interval

Adjust how often usage data refreshes (default 60 seconds):

cache_max_age=60   # seconds between API calls

Troubleshooting

Statusline shows just "Claude": The script received empty input. Make sure the statusLine config is correct in settings.json.

No usage bars (only line 1 shows): The OAuth token couldn't be resolved, or the API call failed. Check that you're logged in to Claude Code with an active session. You can test the token resolution by running:

# macOS
security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null | jq -r '.claudeAiOauth.accessToken'

Bars show 0% but you've been using Claude: The cache may be stale. Delete it and it will refresh:

rm /tmp/claude/statusline-usage-cache.json

License

MIT — do whatever you want with it.

#!/bin/bash
# Line 1: Model | tokens used/total | % used <fullused> | % remain <fullremain> | thinking: on/off
# Line 2: project: <folder> | current: <progressbar> % | weekly: <progressbar> % | extra: <progressbar> $used/$limit
# Line 3: branch: <branch> | resets <time> | resets <datetime> | resets <date>
set -f # disable globbing
input=$(cat)
if [ -z "$input" ]; then
printf "Claude"
exit 0
fi
# ANSI colors matching oh-my-posh theme
blue='\033[38;2;0;153;255m'
orange='\033[38;2;255;176;85m'
green='\033[38;2;0;160;0m'
cyan='\033[38;2;46;149;153m'
red='\033[38;2;255;85;85m'
yellow='\033[38;2;230;200;0m'
white='\033[38;2;220;220;220m'
dim='\033[2m'
reset='\033[0m'
# Format token counts (e.g., 50k / 200k)
format_tokens() {
local num=$1
if [ "$num" -ge 1000000 ]; then
awk "BEGIN {printf \"%.1fm\", $num / 1000000}"
elif [ "$num" -ge 1000 ]; then
awk "BEGIN {printf \"%.0fk\", $num / 1000}"
else
printf "%d" "$num"
fi
}
# Format number with commas (e.g., 134,938)
format_commas() {
printf "%'d" "$1"
}
# Build a colored progress bar
# Usage: build_bar <pct> <width>
build_bar() {
local pct=$1
local width=$2
[ "$pct" -lt 0 ] 2>/dev/null && pct=0
[ "$pct" -gt 100 ] 2>/dev/null && pct=100
local filled=$(( pct * width / 100 ))
local empty=$(( width - filled ))
# Color based on usage level
local bar_color
if [ "$pct" -ge 90 ]; then bar_color="$red"
elif [ "$pct" -ge 70 ]; then bar_color="$yellow"
elif [ "$pct" -ge 50 ]; then bar_color="$orange"
else bar_color="$green"
fi
local filled_str="" empty_str=""
for ((i=0; i<filled; i++)); do filled_str+=""; done
for ((i=0; i<empty; i++)); do empty_str+=""; done
printf "${bar_color}${filled_str}${dim}${empty_str}${reset}"
}
# ===== Extract data from JSON =====
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
# Current working directory
cwd=$(echo "$input" | jq -r '.cwd // empty')
# Git branch, worktree, and dirty state detection
git_branch=""
git_worktree=""
git_dirty=false
if [ -n "$cwd" ] && command -v git >/dev/null 2>&1; then
git_branch=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null)
if [ -n "$git_branch" ]; then
# Check for uncommitted changes
if [ -n "$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" status --porcelain 2>/dev/null)" ]; then
git_dirty=true
fi
# Check if we're in a worktree (not the main working tree)
git_toplevel=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
git_common=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)
git_dir=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" rev-parse --git-dir 2>/dev/null)
if [ -n "$git_dir" ] && [ -n "$git_common" ] && [ "$git_dir" != "$git_common" ]; then
git_worktree=$(basename "$git_toplevel")
fi
fi
fi
# Folder name (last component of cwd)
folder_name=""
if [ -n "$cwd" ]; then
folder_name=$(basename "$cwd")
fi
# Context window
size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
[ "$size" -eq 0 ] 2>/dev/null && size=200000
# Token usage
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_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')
current=$(( input_tokens + cache_create + cache_read ))
used_tokens=$(format_tokens $current)
total_tokens=$(format_tokens $size)
if [ "$size" -gt 0 ]; then
pct_used=$(( current * 100 / size ))
else
pct_used=0
fi
# Check thinking status
thinking_on=false
settings_path="$HOME/.claude/settings.json"
if [ -f "$settings_path" ]; then
thinking_val=$(jq -r '.alwaysThinkingEnabled // false' "$settings_path" 2>/dev/null)
[ "$thinking_val" = "true" ] && thinking_on=true
fi
# ===== LINE 1: Model (% used) | tokens | thinking =====
line1=""
line1+="${blue}${model_name}${reset}"
line1+=" ${dim}|${reset} "
line1+="${green}${pct_used}% used${reset}"
line1+=" ${dim}|${reset} "
line1+="${orange}${used_tokens} / ${total_tokens}${reset}"
line1+=" ${dim}|${reset} "
line1+="thinking: "
if $thinking_on; then
line1+="${orange}On${reset}"
else
line1+="${dim}Off${reset}"
fi
# ===== Cross-platform OAuth token resolution (from statusline.sh) =====
# Tries credential sources in order: env var → macOS Keychain → Linux creds file → GNOME Keyring
get_oauth_token() {
local token=""
# 1. Explicit env var override
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
echo "$CLAUDE_CODE_OAUTH_TOKEN"
return 0
fi
# 2. macOS Keychain
if command -v security >/dev/null 2>&1; then
local blob
blob=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
if [ -n "$blob" ]; then
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
if [ -n "$token" ] && [ "$token" != "null" ]; then
echo "$token"
return 0
fi
fi
fi
# 3. Linux credentials file
local creds_file="${HOME}/.claude/.credentials.json"
if [ -f "$creds_file" ]; then
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null)
if [ -n "$token" ] && [ "$token" != "null" ]; then
echo "$token"
return 0
fi
fi
# 4. GNOME Keyring via secret-tool
if command -v secret-tool >/dev/null 2>&1; then
local blob
blob=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null)
if [ -n "$blob" ]; then
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
if [ -n "$token" ] && [ "$token" != "null" ]; then
echo "$token"
return 0
fi
fi
fi
echo ""
}
# ===== LINE 2 & 3: Usage limits with progress bars (cached) =====
cache_file="/tmp/claude/statusline-usage-cache.json"
lock_file="/tmp/claude/statusline-refresh.lock"
cache_max_age=60 # seconds between API calls
lock_max_age=10 # seconds before considering a lock stale (> curl timeout of 5s)
mkdir -p /tmp/claude
# Always read from cache first (even if stale) — never block on network I/O
usage_data=""
if [ -f "$cache_file" ]; then
usage_data=$(cat "$cache_file" 2>/dev/null)
fi
# Check if cache needs refreshing
needs_refresh=true
if [ -f "$cache_file" ]; then
cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null)
now=$(date +%s)
cache_age=$(( now - cache_mtime ))
[ "$cache_age" -lt "$cache_max_age" ] && needs_refresh=false
fi
# If stale, kick off a background refresh (non-blocking)
if $needs_refresh; then
# Check lock: skip if another refresh is already in progress
launch_refresh=true
if [ -f "$lock_file" ]; then
lock_mtime=$(stat -c %Y "$lock_file" 2>/dev/null || stat -f %m "$lock_file" 2>/dev/null)
now=${now:-$(date +%s)}
lock_age=$(( now - lock_mtime ))
if [ "$lock_age" -lt "$lock_max_age" ]; then
launch_refresh=false # lock is fresh, another fetch is running
else
rm -f "$lock_file" # lock is stale, clear it
fi
fi
if $launch_refresh; then
(
touch "$lock_file"
token=$(get_oauth_token)
if [ -n "$token" ] && [ "$token" != "null" ]; then
response=$(curl -s --max-time 5 \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "User-Agent: claude-code/2.1.34" \
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
if [ -n "$response" ] && echo "$response" | jq . >/dev/null 2>&1; then
echo "$response" > "$cache_file"
fi
fi
rm -f "$lock_file"
) &
disown 2>/dev/null
fi
fi
# Cross-platform ISO to epoch conversion
# Converts ISO 8601 timestamp (e.g. "2025-06-15T12:30:00Z" or "2025-06-15T12:30:00.123+00:00") to epoch seconds.
# Properly handles UTC timestamps and converts to local time.
iso_to_epoch() {
local iso_str="$1"
# Try GNU date first (Linux) — handles ISO 8601 format automatically
local epoch
epoch=$(date -d "${iso_str}" +%s 2>/dev/null)
if [ -n "$epoch" ]; then
echo "$epoch"
return 0
fi
# BSD date (macOS) - handle various ISO 8601 formats
local stripped="${iso_str%%.*}" # Remove fractional seconds (.123456)
stripped="${stripped%%Z}" # Remove trailing Z
stripped="${stripped%%+*}" # Remove timezone offset (+00:00)
stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}" # Remove negative timezone offset
# Check if timestamp is UTC (has Z or +00:00 or -00:00)
if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then
# For UTC timestamps, parse with timezone set to UTC
epoch=$(env TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
else
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
fi
if [ -n "$epoch" ]; then
echo "$epoch"
return 0
fi
return 1
}
# Format ISO reset time to compact local time
# Usage: format_reset_time <iso_string> <style: time|datetime|date>
format_reset_time() {
local iso_str="$1"
local style="$2"
[ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return
# Parse ISO datetime and convert to local time (cross-platform)
local epoch
epoch=$(iso_to_epoch "$iso_str")
[ -z "$epoch" ] && return
# Format based on style (try BSD date first, then GNU date)
# BSD date uses %p (uppercase AM/PM), so convert to lowercase
case "$style" in
time)
date -j -r "$epoch" +"%l:%M%p" 2>/dev/null | sed 's/^ //' | tr '[:upper:]' '[:lower:]' || \
date -d "@$epoch" +"%l:%M%P" 2>/dev/null | sed 's/^ //'
;;
datetime)
date -j -r "$epoch" +"%b %-d, %l:%M%p" 2>/dev/null | sed 's/ / /g; s/^ //' | tr '[:upper:]' '[:lower:]' || \
date -d "@$epoch" +"%b %-d, %l:%M%P" 2>/dev/null | sed 's/ / /g; s/^ //'
;;
*)
date -j -r "$epoch" +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]' || \
date -d "@$epoch" +"%b %-d" 2>/dev/null
;;
esac
}
# Pad column to fixed width (ignoring ANSI codes)
# Usage: pad_column <text_with_ansi> <visible_length> <column_width>
pad_column() {
local text="$1"
local visible_len=$2
local col_width=$3
local padding=$(( col_width - visible_len ))
if [ "$padding" -gt 0 ]; then
printf "%s%*s" "$text" "$padding" ""
else
printf "%s" "$text"
fi
}
line2=""
line3=""
sep=" ${dim}|${reset} "
# ---- Project/branch column (first column on lines 2 & 3) ----
col0_project=""
col0_branch=""
col0_project_plain=""
col0_branch_plain=""
if [ -n "$folder_name" ]; then
col0_project_plain="project: ${folder_name}"
fi
if [ -n "$git_branch" ]; then
branch_display="${git_branch}"
if [ -n "$git_worktree" ]; then
branch_display="${git_branch} (${git_worktree})"
fi
col0_branch_plain="branch: ${branch_display}"
fi
# Dynamically size col0 to fit the longer of project/branch
col0w=${#col0_project_plain}
[ ${#col0_branch_plain} -gt "$col0w" ] && col0w=${#col0_branch_plain}
if [ -n "$col0_project_plain" ]; then
col0_project="${white}project:${reset} ${cyan}${folder_name}${reset}"
col0_project=$(pad_column "$col0_project" "${#col0_project_plain}" "$col0w")
fi
if [ -n "$col0_branch_plain" ]; then
if $git_dirty; then
branch_color="$yellow"
else
branch_color="$green"
fi
col0_branch="${white}branch:${reset} ${branch_color}${branch_display}${reset}"
col0_branch=$(pad_column "$col0_branch" "${#col0_branch_plain}" "$col0w")
elif [ -n "$folder_name" ]; then
col0_branch=$(printf "%*s" "$col0w" "")
fi
if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then
bar_width=10
col1w=23
col2w=22
# ---- 5-hour (current) ----
five_hour_pct=$(echo "$usage_data" | jq -r '.five_hour.utilization // 0' | awk '{printf "%.0f", $1}')
five_hour_reset_iso=$(echo "$usage_data" | jq -r '.five_hour.resets_at // empty')
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
five_hour_bar=$(build_bar "$five_hour_pct" "$bar_width")
# Calculate visible length: "current: " + bar + " " + "XX%"
col1_bar_vis_len=$(( 9 + bar_width + 1 + ${#five_hour_pct} + 1 ))
col1_bar="${white}current:${reset} ${five_hour_bar} ${cyan}${five_hour_pct}%${reset}"
col1_bar=$(pad_column "$col1_bar" "$col1_bar_vis_len" "$col1w")
col1_reset_plain="resets ${five_hour_reset}"
col1_reset="${white}resets ${five_hour_reset}${reset}"
col1_reset=$(pad_column "$col1_reset" "${#col1_reset_plain}" "$col1w")
# ---- 7-day (weekly) ----
seven_day_pct=$(echo "$usage_data" | jq -r '.seven_day.utilization // 0' | awk '{printf "%.0f", $1}')
seven_day_reset_iso=$(echo "$usage_data" | jq -r '.seven_day.resets_at // empty')
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
seven_day_bar=$(build_bar "$seven_day_pct" "$bar_width")
col2_bar_vis_len=$(( 8 + bar_width + 1 + ${#seven_day_pct} + 1 ))
col2_bar="${white}weekly:${reset} ${seven_day_bar} ${cyan}${seven_day_pct}%${reset}"
col2_bar=$(pad_column "$col2_bar" "$col2_bar_vis_len" "$col2w")
col2_reset_plain="resets ${seven_day_reset}"
col2_reset="${white}resets ${seven_day_reset}${reset}"
col2_reset=$(pad_column "$col2_reset" "${#col2_reset_plain}" "$col2w")
# ---- Extra usage ----
col3_bar=""
col3_reset=""
extra_enabled=$(echo "$usage_data" | jq -r '.extra_usage.is_enabled // false')
if [ "$extra_enabled" = "true" ]; then
extra_pct=$(echo "$usage_data" | jq -r '.extra_usage.utilization // 0' | awk '{printf "%.0f", $1}')
extra_used=$(echo "$usage_data" | jq -r '.extra_usage.used_credits // 0' | awk '{printf "%.2f", $1/100}')
extra_limit=$(echo "$usage_data" | jq -r '.extra_usage.monthly_limit // 0' | awk '{printf "%.2f", $1/100}')
extra_bar=$(build_bar "$extra_pct" "$bar_width")
# Next month 1st for reset date (macOS compatible)
extra_reset=$(date -v+1m -v1d +"%b %-d" | tr '[:upper:]' '[:lower:]')
col3_bar="${white}extra:${reset} ${extra_bar} ${cyan}\$${extra_used}/\$${extra_limit}${reset}"
col3_reset="${white}resets ${extra_reset}${reset}"
fi
# Assemble line 2: project + bars row
line2=""
[ -n "$col0_project" ] && line2+="${col0_project}${sep}"
line2+="${col1_bar}${sep}${col2_bar}"
[ -n "$col3_bar" ] && line2+="${sep}${col3_bar}"
# Assemble line 3: branch + resets row
line3=""
[ -n "$col0_branch" ] && line3+="${col0_branch}${sep}"
line3+="${col1_reset}${sep}${col2_reset}"
[ -n "$col3_reset" ] && line3+="${sep}${col3_reset}"
else
# No usage data - still show project/branch if available
[ -n "$col0_project" ] && line2="${col0_project}"
[ -n "$col0_branch" ] && line3="${col0_branch}"
fi
# Output all lines
printf "%b" "$line1"
[ -n "$line2" ] && printf "\n%b" "$line2"
[ -n "$line3" ] && printf "\n%b" "$line3"
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment