Last active
February 9, 2026 17:58
-
-
Save pento/d5cd21682486f83e2328cbb7861262d3 to your computer and use it in GitHub Desktop.
Claude Code usage status line: save the scripts in your ~/.claude/scripts directory, and add the settings to your ~/.claude/settings.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| CACHE_DIR="$HOME/.claude/cache" | |
| CACHE_FILE="$CACHE_DIR/claude-usage.json" | |
| TTL_SECONDS=900 # 15 minutes | |
| mkdir -p "$CACHE_DIR" | |
| # Check cache age unless --force | |
| if [[ "${1:-}" != "--force" ]] && [[ -f "$CACHE_FILE" ]]; then | |
| now=$(date +%s) | |
| mod=$(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) | |
| age=$(( now - mod )) | |
| if (( age < TTL_SECONDS )); then | |
| exit 0 | |
| fi | |
| fi | |
| write_error() { | |
| local err="$1" | |
| local now_ms=$(( $(date +%s) * 1000 )) | |
| cat > "$CACHE_FILE" <<EJSON | |
| {"error":"$err","fetched_at":$now_ms} | |
| EJSON | |
| } | |
| # Extract OAuth token - try credentials file first, then fall back to keychain | |
| CREDS_FILE="$HOME/.claude/.credentials.json" | |
| if [[ -f "$CREDS_FILE" ]]; then | |
| cred_json=$(cat "$CREDS_FILE" 2>/dev/null) || { | |
| write_error "credentials_file_read_error" | |
| exit 0 | |
| } | |
| else | |
| cred_json=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) || { | |
| write_error "keychain_error" | |
| exit 0 | |
| } | |
| fi | |
| access_token=$(printf '%s' "$cred_json" | /usr/bin/python3 -c " | |
| import sys, json | |
| d = json.load(sys.stdin) | |
| print(d['claudeAiOauth']['accessToken']) | |
| " 2>/dev/null) || { | |
| write_error "token_parse_error" | |
| exit 0 | |
| } | |
| # Make a minimal API call to read rate-limit headers | |
| # Using claude-haiku-4-5-20251001 with 1 max_token for minimal cost | |
| tmpbody=$(mktemp) | |
| response=$(curl -sS -D- -o "$tmpbody" \ | |
| -X POST https://api.anthropic.com/v1/messages \ | |
| -H "Authorization: Bearer $access_token" \ | |
| -H "anthropic-version: 2023-06-01" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| -H "content-type: application/json" \ | |
| -d '{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"x"}]}' \ | |
| 2>/dev/null) || { | |
| rm -f "$tmpbody" | |
| write_error "api_request_error" | |
| exit 0 | |
| } | |
| # Check for HTTP errors | |
| http_status=$(echo "$response" | head -1 | grep -oE '[0-9]{3}' | head -1 || true) | |
| if [[ "$http_status" != "200" ]]; then | |
| body=$(cat "$tmpbody" 2>/dev/null | head -c 300 | tr '"' "'" || echo "") | |
| rm -f "$tmpbody" | |
| # Also log token length for debugging (not the token itself) | |
| token_len=${#access_token} | |
| write_error "api_error_${http_status:-unknown}|token_len=${token_len}|$body" | |
| exit 0 | |
| fi | |
| rm -f "$tmpbody" | |
| # Extract rate-limit headers (case-insensitive) | |
| # Actual header names: anthropic-ratelimit-unified-* | |
| # Note: grep returns exit code 1 on no match; use || true to avoid pipefail exit | |
| util_5h=$(echo "$response" | grep -i '^anthropic-ratelimit-unified-5h-utilization:' | tr -d '[:space:]' | cut -d: -f2 || true) | |
| util_7d=$(echo "$response" | grep -i '^anthropic-ratelimit-unified-7d-utilization:' | tr -d '[:space:]' | cut -d: -f2 || true) | |
| status_5h=$(echo "$response" | grep -i '^anthropic-ratelimit-unified-5h-status:' | tr -d '[:space:]' | cut -d: -f2 || true) | |
| reset_5h=$(echo "$response" | grep -i '^anthropic-ratelimit-unified-5h-reset:' | tr -d '[:space:]' | cut -d: -f2 || true) | |
| reset_7d=$(echo "$response" | grep -i '^anthropic-ratelimit-unified-7d-reset:' | tr -d '[:space:]' | cut -d: -f2 || true) | |
| # If none of the expected headers are present, dump all headers for debugging | |
| if [[ -z "$util_5h" && -z "$util_7d" && -z "$status_5h" ]]; then | |
| all_headers=$(echo "$response" | grep -i ':' | tr '\r' ' ' | tr '\n' '|' || true) | |
| write_error "no_utilization_headers|headers:$all_headers" | |
| exit 0 | |
| fi | |
| # Convert utilization floats to percentages (0.81 -> 81) | |
| pct_5h="" | |
| pct_7d="" | |
| if [[ -n "$util_5h" ]]; then | |
| pct_5h=$(/usr/bin/python3 -c "print(int(float('$util_5h') * 100 + 0.5))" 2>/dev/null || echo "") | |
| fi | |
| if [[ -n "$util_7d" ]]; then | |
| pct_7d=$(/usr/bin/python3 -c "print(int(float('$util_7d') * 100 + 0.5))" 2>/dev/null || echo "") | |
| fi | |
| now_ms=$(( $(date +%s) * 1000 )) | |
| cat > "$CACHE_FILE" <<EJSON | |
| {"used_5h_percent":${pct_5h:-null},"used_7d_percent":${pct_7d:-null},"reset_5h":${reset_5h:-null},"reset_7d":${reset_7d:-null},"status":"${status_5h:-unknown}","fetched_at":$now_ms} | |
| EJSON |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| CACHE_FILE="$HOME/.claude/cache/claude-usage.json" | |
| WINDOW_5H=18000 # 5 hours in seconds | |
| WINDOW_7D=604800 # 7 days in seconds | |
| # Read stdin JSON from Claude Code | |
| input=$(cat) | |
| # Extract model display name and context % via stdin (safe for any JSON content) | |
| model=$(printf '%s' "$input" | /usr/bin/python3 -c " | |
| import sys, json | |
| d = json.load(sys.stdin) | |
| print(d.get('model', {}).get('display_name', '--')) | |
| " 2>/dev/null || echo "--") | |
| ctx=$(printf '%s' "$input" | /usr/bin/python3 -c " | |
| import sys, json | |
| d = json.load(sys.stdin) | |
| print(int(d.get('context_window', {}).get('used_percentage', 0))) | |
| " 2>/dev/null || echo "0") | |
| # ANSI color helpers | |
| reset='\033[0m' | |
| green='\033[32m' | |
| yellow='\033[33m' | |
| red='\033[31m' | |
| dim='\033[2m' | |
| # Color based on usage vs expected pace through window | |
| # delta = actual_pct - expected_pct (where expected = time_elapsed_fraction * 100) | |
| # green: well below pace (delta < -5) | |
| # yellow: near pace (-5 <= delta <= 5) | |
| # red: above pace (delta > 5) | |
| color_for_pace() { | |
| local actual=$1 | |
| local expected=$2 | |
| local delta=$(( actual - expected )) | |
| if (( delta > 5 )); then | |
| echo -ne "$red" | |
| elif (( delta >= -5 )); then | |
| echo -ne "$yellow" | |
| else | |
| echo -ne "$green" | |
| fi | |
| } | |
| # Simple color for absolute percentage (used for context window) | |
| color_for_pct() { | |
| local pct=$1 | |
| if (( pct > 70 )); then | |
| echo -ne "$red" | |
| elif (( pct > 40 )); then | |
| echo -ne "$yellow" | |
| else | |
| echo -ne "$green" | |
| fi | |
| } | |
| # Build progress bar with time-position marker (10 chars wide) | |
| # Usage: progress_bar <usage_pct> <time_pct> | |
| # ███░░░░▏░░ = usage at 30%, time marker at 70% | |
| progress_bar_with_marker() { | |
| local pct=$1 | |
| local time_pct=$2 | |
| local filled=$(( pct / 10 )) | |
| local marker_pos=$(( time_pct / 10 )) | |
| # Clamp to 0-9 range (marker is placed within the 10-char bar) | |
| (( marker_pos > 9 )) && marker_pos=9 | |
| (( marker_pos < 0 )) && marker_pos=0 | |
| local bar="" | |
| for ((i=0; i<10; i++)); do | |
| if (( i == marker_pos )); then | |
| bar+="*" | |
| elif (( i < filled )); then | |
| bar+="█" | |
| else | |
| bar+="░" | |
| fi | |
| done | |
| echo -n "$bar" | |
| } | |
| # Read cache file | |
| pct_5h="--" | |
| pct_7d="--" | |
| reset_5h="" | |
| reset_7d="" | |
| status="" | |
| stale_note="" | |
| time_pct_5h=0 | |
| time_pct_7d=0 | |
| if [[ -f "$CACHE_FILE" ]]; then | |
| cache=$(<"$CACHE_FILE") | |
| # Parse all cache values in one python call via stdin | |
| eval "$(printf '%s' "$cache" | /usr/bin/python3 -c " | |
| import sys, json | |
| d = json.load(sys.stdin) | |
| if 'error' not in d: | |
| v5 = d.get('used_5h_percent') | |
| v7 = d.get('used_7d_percent') | |
| print(f'pct_5h=\"{v5 if v5 is not None else \"--\"}\"') | |
| print(f'pct_7d=\"{v7 if v7 is not None else \"--\"}\"') | |
| print(f'status=\"{d.get(\"status\", \"\")}\"') | |
| r5 = d.get('reset_5h') | |
| r7 = d.get('reset_7d') | |
| if r5 is not None: print(f'reset_5h={int(r5)}') | |
| if r7 is not None: print(f'reset_7d={int(r7)}') | |
| print(f'fetched_at={d.get(\"fetched_at\", 0)}') | |
| " 2>/dev/null)" 2>/dev/null || true | |
| now=$(date +%s) | |
| # Calculate time elapsed as percentage of each window | |
| if [[ -n "$reset_5h" ]]; then | |
| remaining_5h=$(( reset_5h - now )) | |
| (( remaining_5h < 0 )) && remaining_5h=0 | |
| elapsed_5h=$(( WINDOW_5H - remaining_5h )) | |
| time_pct_5h=$(( elapsed_5h * 100 / WINDOW_5H )) | |
| (( time_pct_5h > 100 )) && time_pct_5h=100 | |
| (( time_pct_5h < 0 )) && time_pct_5h=0 | |
| fi | |
| if [[ -n "$reset_7d" ]]; then | |
| remaining_7d=$(( reset_7d - now )) | |
| (( remaining_7d < 0 )) && remaining_7d=0 | |
| elapsed_7d=$(( WINDOW_7D - remaining_7d )) | |
| time_pct_7d=$(( elapsed_7d * 100 / WINDOW_7D )) | |
| (( time_pct_7d > 100 )) && time_pct_7d=100 | |
| (( time_pct_7d < 0 )) && time_pct_7d=0 | |
| fi | |
| # Check staleness | |
| now_ms=$(( now * 1000 )) | |
| age_min=$(( (now_ms - ${fetched_at:-0}) / 60000 )) | |
| if (( age_min > 20 )); then | |
| stale_note=" ${dim}(${age_min}m ago)${reset}" | |
| fi | |
| fi | |
| # Build output | |
| output="" | |
| # Model name | |
| output+="$model" | |
| output+=" │ " | |
| # 5h utilization with time marker | |
| if [[ "$pct_5h" != "--" ]]; then | |
| output+="5h: $(color_for_pace "$pct_5h" "$time_pct_5h")$(progress_bar_with_marker "$pct_5h" "$time_pct_5h") ${pct_5h}%${reset}" | |
| else | |
| output+="5h: ${dim}--${reset}" | |
| fi | |
| output+=" │ " | |
| # 7d utilization (color based on pace) | |
| if [[ "$pct_7d" != "--" ]]; then | |
| output+="7d: $(color_for_pace "$pct_7d" "$time_pct_7d")${pct_7d}%${reset}" | |
| else | |
| output+="7d: ${dim}--${reset}" | |
| fi | |
| output+=" │ " | |
| # Context window | |
| output+="ctx: $(color_for_pct "$ctx")${ctx}%${reset}" | |
| # Rate limited warning | |
| if [[ "$status" == "rate_limited" ]]; then | |
| output+=" ${red}⚠ RATE LIMITED${reset}" | |
| fi | |
| # Staleness | |
| output+="$stale_note" | |
| echo -ne "$output" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "bash ~/.claude/scripts/refresh-usage.sh --force", | |
| "timeout": 15 | |
| } | |
| ] | |
| } | |
| ], | |
| "Stop": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "bash ~/.claude/scripts/refresh-usage.sh", | |
| "timeout": 15 | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| "statusLine": { | |
| "type": "command", | |
| "command": "bash ~/.claude/scripts/statusline.sh" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment