Last active
March 10, 2026 15:45
-
-
Save kazuph/418b604da3ff5538eb92c5feda40d141 to your computer and use it in GitHub Desktop.
Claude Code statusline script — 3-bar display (context window, 5h usage, 7d usage) with color-coded progress bars, cost in JPY, git branch, and API usage limits from Anthropic OAuth. macOS only.
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
| #!/bin/bash | |
| input=$(cat) | |
| USD_JPY=150 # Fixed rate — update occasionally | |
| # --- Usage Limits (API fetch with cache) --- | |
| USAGE_CACHE="/tmp/claude-statusline-usage.json" | |
| USAGE_STAMP="/tmp/claude-statusline-usage.stamp" | |
| USAGE_LOCK="/tmp/claude-statusline-usage.lock" | |
| USAGE_CACHE_AGE=300 # seconds between successful refreshes | |
| USAGE_RETRY_AGE=60 # seconds between retry after failure | |
| USAGE_STALE_MAX=1800 # seconds — hide usage if data older than 30 min | |
| fetch_usage() { | |
| local token="" creds="" | |
| creds=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) || return 1 | |
| token=$(echo "$creds" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| [[ -z "$token" ]] && return 1 | |
| local tmp="${USAGE_CACHE}.tmp.$$" | |
| if curl -s --max-time 3 "https://api.anthropic.com/api/oauth/usage" \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| -H "Content-Type: application/json" -o "$tmp" 2>/dev/null \ | |
| && [[ -s "$tmp" ]] \ | |
| && jq -e '.five_hour' "$tmp" >/dev/null 2>&1; then | |
| mv -f "$tmp" "$USAGE_CACHE" | |
| else | |
| rm -f "$tmp" | |
| fi | |
| # Always update stamp (tracks last attempt, not last success) | |
| date +%s > "$USAGE_STAMP" | |
| } | |
| # Stale lock recovery: if lock is older than 30s, the holder likely crashed | |
| recover_stale_lock() { | |
| local lock_pid_file="$USAGE_LOCK/pid" | |
| if [[ -d "$USAGE_LOCK" ]]; then | |
| local lock_age=$(( $(date +%s) - $(stat -f %m "$USAGE_LOCK" 2>/dev/null || echo 0) )) | |
| if (( lock_age > 30 )); then | |
| # Lock holder likely dead — reclaim | |
| rm -rf "$USAGE_LOCK" | |
| return 0 | |
| fi | |
| # Check if PID is still alive | |
| if [[ -f "$lock_pid_file" ]]; then | |
| local lock_pid | |
| lock_pid=$(<"$lock_pid_file") | |
| if ! kill -0 "$lock_pid" 2>/dev/null; then | |
| rm -rf "$USAGE_LOCK" | |
| return 0 | |
| fi | |
| fi | |
| return 1 # lock is valid | |
| fi | |
| return 0 # no lock exists | |
| } | |
| # Decide whether to refresh | |
| needs_refresh=false | |
| NOW=$(date +%s) | |
| LAST_ATTEMPT=$(cat "$USAGE_STAMP" 2>/dev/null || echo 0) | |
| if [[ ! -f "$USAGE_CACHE" ]]; then | |
| needs_refresh=true | |
| elif (( NOW - $(stat -f %m "$USAGE_CACHE" 2>/dev/null || echo 0) > USAGE_CACHE_AGE )) \ | |
| && (( NOW - LAST_ATTEMPT > USAGE_RETRY_AGE )); then | |
| needs_refresh=true | |
| fi | |
| if $needs_refresh; then | |
| recover_stale_lock | |
| if mkdir "$USAGE_LOCK" 2>/dev/null; then | |
| echo $$ > "$USAGE_LOCK/pid" | |
| fetch_usage | |
| rm -rf "$USAGE_LOCK" | |
| fi | |
| fi | |
| # Read usage cache in one jq call (performance: avoid 3 separate jq invocations) | |
| if [[ -f "$USAGE_CACHE" ]]; then | |
| CACHE_AGE=$(( NOW - $(stat -f %m "$USAGE_CACHE" 2>/dev/null || echo 0) )) | |
| if (( CACHE_AGE < USAGE_STALE_MAX )); then | |
| eval "$(jq -r ' | |
| "USAGE_5H=" + (.five_hour.utilization // 0 | floor | tostring), | |
| "USAGE_7D=" + (.seven_day.utilization // 0 | floor | tostring), | |
| "RESETS_5H=" + (.five_hour.resets_at // "" | @sh) | |
| ' "$USAGE_CACHE" 2>/dev/null)" | |
| fi | |
| fi | |
| # Parse stdin JSON — single jq call for all fields | |
| eval "$(echo "$input" | jq -r ' | |
| "MODEL=" + (@sh "\(.model.display_name // "?")"), | |
| "CWD=" + (@sh "\(.workspace.current_dir // "")"), | |
| "PROJECT_DIR=" + (@sh "\(.workspace.project_dir // "")"), | |
| "PCT=" + (@sh "\(.context_window.used_percentage // 0 | floor | tostring)"), | |
| "CTX_SIZE=" + (@sh "\(.context_window.context_window_size // 200000 | tostring)"), | |
| "LINES_ADD=" + (@sh "\(.cost.total_lines_added // 0 | tostring)"), | |
| "LINES_DEL=" + (@sh "\(.cost.total_lines_removed // 0 | tostring)"), | |
| "WT_BRANCH=" + (@sh "\(.worktree.branch // "")"), | |
| "COST_USD=" + (@sh "\(.cost.total_cost_usd // 0 | tostring)") | |
| ' 2>/dev/null)" || true | |
| # --- Colors --- | |
| RST='\033[0m'; DIM='\033[2m'; BOLD='\033[1m' | |
| CYAN='\033[36m'; MAGENTA='\033[35m'; BLUE='\033[34m' | |
| GREEN='\033[32m'; RED='\033[31m'; YELLOW='\033[33m' | |
| # Git branch — prefer WT_BRANCH from stdin JSON, fallback to git command | |
| BRANCH="${WT_BRANCH}" | |
| if [[ -z "$BRANCH" && -n "$PROJECT_DIR" ]] && command -v git >/dev/null 2>&1; then | |
| BRANCH=$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") | |
| fi | |
| # Replace $HOME with ~ | |
| DIR_PATH="${CWD/#$HOME/~}" | |
| # NerdFont git branch icon | |
| GIT_ICON=$'\xee\x82\xa0' # U+E0A0 | |
| # --- Line 1: model | branch | dir --- | |
| L1="${BOLD}${CYAN}${MODEL}${RST} ${DIM}|${RST} " | |
| if [[ -n "$BRANCH" ]]; then | |
| L1+="${MAGENTA}${GIT_ICON} ${BRANCH}${RST}" | |
| if (( LINES_ADD > 0 || LINES_DEL > 0 )); then | |
| L1+=" ${GREEN}+${LINES_ADD}${RST}${DIM}/${RST}${RED}-${LINES_DEL}${RST}" | |
| fi | |
| L1+=" ${DIM}|${RST} " | |
| fi | |
| L1+="${BLUE}${DIR_PATH}${RST}" | |
| # --- Helper: build a bar --- | |
| make_bar() { | |
| local pct=$1 width=$2 color=$3 | |
| (( pct > 100 )) && pct=100 | |
| (( pct < 0 )) && pct=0 | |
| local filled=$((pct * width / 100)) | |
| local empty=$((width - filled)) | |
| local bar="" | |
| for ((i=0; i<filled; i++)); do bar+="▓"; done | |
| for ((i=0; i<empty; i++)); do bar+="░"; done | |
| printf '%b' "${color}${bar}${RST}" | |
| } | |
| # Color by percentage | |
| bar_color() { | |
| local pct=$1 | |
| if (( pct < 50 )); then printf '%b' "$GREEN" | |
| elif (( pct < 80 )); then printf '%b' "$YELLOW" | |
| else printf '%b' "$RED"; fi | |
| } | |
| # --- Line 2: three bars (context | 5h | 7d) + cost --- | |
| BAR_WIDTH=10 | |
| # Context size label | |
| if (( CTX_SIZE >= 1000000 )); then CTX_LABEL="1M"; else CTX_LABEL="200k"; fi | |
| # Cost in JPY | |
| COST_JPY=$(awk "BEGIN { printf \"%.0f\", ${COST_USD:-0} * ${USD_JPY} }") | |
| # Usage percentages (clamp 0..100) | |
| U5H_PCT=$(( ${USAGE_5H:-0} > 100 ? 100 : ${USAGE_5H:-0} )) | |
| U7D_PCT=$(( ${USAGE_7D:-0} > 100 ? 100 : ${USAGE_7D:-0} )) | |
| (( U5H_PCT < 0 )) && U5H_PCT=0 | |
| (( U7D_PCT < 0 )) && U7D_PCT=0 | |
| CTX_C=$(bar_color "$PCT") | |
| U5H_C=$(bar_color "$U5H_PCT") | |
| U7D_C=$(bar_color "$U7D_PCT") | |
| # Reset time remaining for 5h block (pure shell — no python3) | |
| RESET_LABEL="" | |
| if [[ -n "$RESETS_5H" && "$RESETS_5H" != "null" ]]; then | |
| # Normalize ISO 8601: remove fractional seconds, +09:00 -> +0900, Z -> +0000 | |
| NORM_TS=$(printf '%s' "$RESETS_5H" | sed -E 's/([+-][0-9]{2}):([0-9]{2})$/\1\2/; s/\.[0-9]+([+-][0-9]{4})$/\1/; s/Z$/+0000/') | |
| RESET_TS=$(date -j -f '%Y-%m-%dT%H:%M:%S%z' "$NORM_TS" +%s 2>/dev/null || echo 0) | |
| if (( RESET_TS > NOW )); then | |
| REMAIN_S=$((RESET_TS - NOW)) | |
| REMAIN_H=$((REMAIN_S / 3600)) | |
| REMAIN_M=$(((REMAIN_S % 3600) / 60)) | |
| RESET_LABEL="${DIM}(${REMAIN_H}h${REMAIN_M}m)${RST}" | |
| fi | |
| fi | |
| L2="${YELLOW}${BOLD}¥${COST_JPY}${RST}" | |
| L2+=" ${DIM}ctx${RST} $(make_bar "$PCT" "$BAR_WIDTH" "$CTX_C") ${CTX_C}${BOLD}${PCT}%${RST}${DIM}/${CTX_LABEL}${RST}" | |
| if (( U5H_PCT > 0 || U7D_PCT > 0 )) || [[ -n "${USAGE_5H}" ]]; then | |
| L2+=" ${DIM}5h${RST} $(make_bar "$U5H_PCT" "$BAR_WIDTH" "$U5H_C") ${U5H_C}${BOLD}${U5H_PCT}%${RST}" | |
| [[ -n "$RESET_LABEL" ]] && L2+="${RESET_LABEL}" | |
| L2+=" ${DIM}7d${RST} $(make_bar "$U7D_PCT" "$BAR_WIDTH" "$U7D_C") ${U7D_C}${BOLD}${U7D_PCT}%${RST}" | |
| fi | |
| printf '%b\n' "$L1" | |
| printf '%b\n' "$L2" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment