Skip to content

Instantly share code, notes, and snippets.

@v3rron
Last active July 31, 2026 18:51
Show Gist options
  • Select an option

  • Save v3rron/67f6279d78dd343a71156f1acaefab34 to your computer and use it in GitHub Desktop.

Select an option

Save v3rron/67f6279d78dd343a71156f1acaefab34 to your computer and use it in GitHub Desktop.
Claude Code custom statusLine
#!/bin/bash
# Row 1: Model | dir@branch | tokens (%used) | effort
# Row 2: 5h %@reset | 7d %@reset | extra used/limit
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"
}
# Return color escape based on usage percentage
# Usage: usage_color <pct>
usage_color() {
local pct=$1
if [ "$pct" -ge 90 ]; then echo "$red"
elif [ "$pct" -ge 70 ]; then echo "$orange"
elif [ "$pct" -ge 50 ]; then echo "$yellow"
else echo "$green"
fi
}
# ===== Extract data from JSON =====
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
# 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
pct_remain=$(( 100 - pct_used ))
used_comma=$(format_commas $current)
remain_comma=$(format_commas $(( size - current )))
# Reasoning effort — live resolved value from stdin (absent if model lacks effort support)
settings_path="$HOME/.claude/settings.json"
effort_level=$(echo "$input" | jq -r '.effort.level // empty')
if [ -z "$effort_level" ]; then
# fallback for older CC / models without the field
if [ -n "$CLAUDE_CODE_EFFORT_LEVEL" ]; then
effort_level="$CLAUDE_CODE_EFFORT_LEVEL"
elif [ -f "$settings_path" ]; then
effort_level=$(jq -r '.effortLevel // empty' "$settings_path" 2>/dev/null)
fi
fi
# ===== Build row 1: model | dir | tokens | effort =====
out=""
out+="${blue}${model_name}${reset}"
# Current working directory
cwd=$(echo "$input" | jq -r '.cwd // empty')
if [ -n "$cwd" ]; then
display_dir="${cwd##*/}"
git_branch=$(git -C "${cwd}" rev-parse --abbrev-ref HEAD 2>/dev/null)
out+=" ${dim}|${reset} "
out+="${cyan}${display_dir}${reset}"
if [ -n "$git_branch" ]; then
out+="${dim}@${reset}${green}${git_branch}${reset}"
git_stat=$(git -C "${cwd}" diff --numstat 2>/dev/null | awk '{a+=$1; d+=$2} END {if (a+d>0) printf "+%d -%d", a, d}')
[ -n "$git_stat" ] && out+=" ${dim}(${reset}${green}${git_stat%% *}${reset} ${red}${git_stat##* }${reset}${dim})${reset}"
fi
fi
out+=" ${dim}|${reset} "
out+="${orange}${used_tokens}/${total_tokens}${reset} ${dim}(${reset}${green}${pct_used}%${reset}${dim})${reset}"
if [ -n "$effort_level" ]; then
out+=" ${dim}|${reset} "
out+="effort: "
case "$effort_level" in
low) out+="${dim}low${reset}" ;;
medium) out+="${orange}med${reset}" ;;
high) out+="${green}high${reset}" ;;
xhigh) out+="${green}xhigh${reset}" ;;
max) out+="${green}max${reset}" ;;
*) out+="${green}${effort_level}${reset}" ;;
esac
fi
# ===== Cross-platform OAuth token resolution =====
# 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 ""
}
# ===== Extra-usage credits (OAuth usage API, cached) =====
# 5h/7d windows come from the stdin payload below; this fetch exists only for
# extra_usage credits, which the statusLine JSON does not expose.
cache_file="/tmp/claude/statusline-usage-cache.json"
cache_max_age=60 # seconds between API calls
mkdir -p /tmp/claude
needs_refresh=true
usage_data=""
# Check cache
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 ))
if [ "$cache_age" -lt "$cache_max_age" ]; then
needs_refresh=false
usage_data=$(cat "$cache_file" 2>/dev/null)
fi
fi
# Fetch fresh data if cache is stale
if $needs_refresh; then
token=$(get_oauth_token)
if [ -n "$token" ] && [ "$token" != "null" ]; then
response=$(curl -s --max-time 10 \
-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
usage_data="$response"
echo "$response" > "$cache_file"
fi
fi
# Fall back to stale cache
if [ -z "$usage_data" ] && [ -f "$cache_file" ]; then
usage_data=$(cat "$cache_file" 2>/dev/null)
fi
fi
# Format Unix epoch seconds to compact local time (rate_limits.*.resets_at is epoch)
format_epoch_time() {
local epoch="$1"
local style="$2"
{ [ -z "$epoch" ] || [ "$epoch" = "null" ]; } && return
case "$style" in
time)
date -d "@$epoch" +"%H:%M" 2>/dev/null || date -j -r "$epoch" +"%H:%M" 2>/dev/null
;;
datetime)
date -d "@$epoch" +"%b %-d, %H:%M" 2>/dev/null || date -j -r "$epoch" +"%b %-d, %H:%M" 2>/dev/null
;;
*)
date -d "@$epoch" +"%b %-d" 2>/dev/null || date -j -r "$epoch" +"%b %-d" 2>/dev/null
;;
esac
}
sep=" ${dim}|${reset} "
# Second row: usage / rate-limit cluster, kept off the main row so it stays readable.
# Each segment is optional; prepend the separator only when the row already has content.
line2=""
# ---- 5-hour & 7-day rate limits (from stdin payload; Pro/Max only, windows independently absent) ----
five_hour_raw=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
if [ -n "$five_hour_raw" ]; then
five_hour_pct=$(echo "$five_hour_raw" | awk '{printf "%.0f", $1}')
five_hour_reset=$(format_epoch_time "$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')" "time")
five_hour_color=$(usage_color "$five_hour_pct")
[ -n "$line2" ] && line2+="$sep"
line2+="${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}"
[ -n "$five_hour_reset" ] && line2+=" ${dim}@${five_hour_reset}${reset}"
fi
seven_day_raw=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
if [ -n "$seven_day_raw" ]; then
seven_day_pct=$(echo "$seven_day_raw" | awk '{printf "%.0f", $1}')
seven_day_reset=$(format_epoch_time "$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')" "datetime")
seven_day_color=$(usage_color "$seven_day_pct")
[ -n "$line2" ] && line2+="$sep"
line2+="${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}"
[ -n "$seven_day_reset" ] && line2+=" ${dim}@${seven_day_reset}${reset}"
fi
# ---- Extra-usage credits (only exposed by the OAuth usage API, not the stdin payload) ----
if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then
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' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}')
extra_limit=$(echo "$usage_data" | jq -r '.extra_usage.monthly_limit // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}')
[ -n "$line2" ] && line2+="$sep"
if [ -n "$extra_used" ] && [ -n "$extra_limit" ]; then
extra_color=$(usage_color "$extra_pct")
line2+="${white}extra${reset} ${extra_color}${extra_used}/${extra_limit}${reset}"
else
line2+="${white}extra${reset} ${green}enabled${reset}"
fi
fi
fi
# Output: row 1 = identity/context/effort; row 2 = usage cluster (only if present)
printf "%b" "$out"
[ -n "$line2" ] && printf "\n%b" "$line2"
exit 0

Claude Code two-row statusline

A custom statusLine script for Claude Code. Identity and context on the top row, usage limits on the bottom:

Opus 5 (1M context) | myrepo@feature/foo (+42 -7) | 191k/1.0m (19%) | effort: high
5h 21% @14:30 | 7d 63% @Aug 3, 09:00 | extra 4.10/25.00

Row 1 — identity & context

Segment Meaning
Opus 5 (1M context) Active model, as Claude Code reports its display name
myrepo@feature/foo Current directory @ git branch
(+42 -7) Unstaged lines added / deleted
191k/1.0m (19%) Context tokens used / window size, and percent consumed
effort: high Reasoning effort level

Row 2 — usage limits

Segment Meaning
5h 21% @14:30 5-hour rate-limit window consumed, and reset time
7d 63% @Aug 3, 09:00 7-day rate-limit window consumed, and reset time
extra 4.10/25.00 Extra-usage credits spent / monthly limit, in dollars

Every segment is optional and renders only when Claude Code supplies its data, so the rows adapt to your plan and model. Row 2's percentages are color-coded — green below 50%, yellow at 50%, orange at 70%, red at 90%. Row 2 is dropped entirely when none of its segments have data.

Requirements

  • bash and jq — required
  • curl — only for the extra-usage credits segment; the script works without it
  • git — optional, for the branch and diff-stat segment

Install

  1. Save statusline.sh to ~/.claude/statusline.sh
  2. Make it executable:
    chmod +x ~/.claude/statusline.sh
  3. Add this to ~/.claude/settings.json:
    {
      "statusLine": {
        "type": "command",
        "command": "~/.claude/statusline.sh"
      }
    }

Claude Code picks it up on the next render — no restart needed.

How the segments resolve

Most of the data arrives on stdin as JSON from Claude Code: model name, cwd, context-window usage, effort.level, and rate_limits. If stdin is empty the script prints Claude and exits.

Reasoning effort falls back, in order:

  1. .effort.level from stdin
  2. $CLAUDE_CODE_EFFORT_LEVEL
  3. .effortLevel in ~/.claude/settings.json

Omitted entirely if none resolve — e.g. models without effort support.

Extra-usage credits are the one thing the stdin payload does not expose, so the script fetches them from the OAuth usage API. The token is resolved from, in order:

  1. $CLAUDE_CODE_OAUTH_TOKEN
  2. macOS Keychain (security find-generic-password -s "Claude Code-credentials")
  3. ~/.claude/.credentials.json
  4. GNOME Keyring (secret-tool)

Responses are cached at /tmp/claude/statusline-usage-cache.json for 60s, falling back to the stale cache if a fetch fails. If no token is found the fetch silently no-ops and the segment is dropped — everything else still renders.

Customizing

  • Colors — truecolor ANSI escapes defined at the top of the script; edit those variables to match your terminal theme. The defaults track an oh-my-posh palette.
  • Cache intervalcache_max_age, in seconds.
  • Thresholds — the usage_color function.

Notes

  • The git diff stat counts unstaged changes only (git diff --numstat).
  • The pinned User-Agent: claude-code/2.1.34 header is cosmetic; bump or leave it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment