#!/bin/bash set -f input=$(cat) if [ -z "$input" ]; then printf "Claude" exit 0 fi # ── Colors ────────────────────────────────────────────── blue='\033[38;2;0;153;255m' orange='\033[38;2;255;176;85m' green='\033[38;2;0;175;80m' cyan='\033[38;2;86;182;194m' red='\033[38;2;255;85;85m' yellow='\033[38;2;230;200;0m' white='\033[38;2;220;220;220m' magenta='\033[38;2;180;140;255m' dim='\033[2m' bold='\033[1m' reset='\033[0m' sep=" ${dim}│${reset} " # ── Helpers ───────────────────────────────────────────── 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 } # Reads statusLine.warnThreshold / statusLine.dangerThreshold from settings.json # (defaults: 50 / 80). Drives all green → yellow → red color transitions. color_for_pct() { local pct=$1 local _warn=50 _danger=80 local _settings="$HOME/.claude/settings.json" if [ -f "$_settings" ]; then local _w _d _w=$(jq -r '.statusLine.warnThreshold // empty' "$_settings" 2>/dev/null) _d=$(jq -r '.statusLine.dangerThreshold // empty' "$_settings" 2>/dev/null) [ -n "$_w" ] && [ "$_w" -eq "$_w" ] 2>/dev/null && _warn=$_w [ -n "$_d" ] && [ "$_d" -eq "$_d" ] 2>/dev/null && _danger=$_d fi if [ "$pct" -ge "$_danger" ]; then printf "$red" elif [ "$pct" -ge "$_warn" ]; then printf "$yellow" else printf "$green" fi } 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 )) local bar_color bar_color=$(color_for_pct "$pct") local filled_str="" empty_str="" for ((i=0; i/dev/null) if [ -n "$epoch" ]; then echo "$epoch" return 0 fi local stripped="${iso_str%%.*}" stripped="${stripped%%Z}" stripped="${stripped%%+*}" stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}" if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then 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_reset_time() { local iso_str="$1" local style="$2" [ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return local epoch epoch=$(iso_to_epoch "$iso_str") [ -z "$epoch" ] && return local result="" case "$style" in time) result=$(date -j -r "$epoch" +"%l:%M%p" 2>/dev/null | sed 's/^ //; s/\.//g' | tr '[:upper:]' '[:lower:]') [ -z "$result" ] && result=$(date -d "@$epoch" +"%l:%M%P" 2>/dev/null | sed 's/^ //; s/\.//g') ;; datetime) result=$(date -j -r "$epoch" +"%b %-d, %l:%M%p" 2>/dev/null | sed 's/ / /g; s/^ //; s/\.//g' | tr '[:upper:]' '[:lower:]') [ -z "$result" ] && result=$(date -d "@$epoch" +"%b %-d, %l:%M%P" 2>/dev/null | sed 's/ / /g; s/^ //; s/\.//g') ;; *) result=$(date -j -r "$epoch" +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]') [ -z "$result" ] && result=$(date -d "@$epoch" +"%b %-d" 2>/dev/null) ;; esac printf "%s" "$result" } # Returns "in 47m", "in 1h23m", or "in 2d5h"; silent if reset is past or epoch unknown. format_countdown() { local epoch="$1" [ -z "$epoch" ] && return local now_e diff now_e=$(date +%s) diff=$(( epoch - now_e )) [ "$diff" -le 0 ] && return if [ "$diff" -ge 86400 ]; then printf "in %dd%dh" "$(( diff / 86400 ))" "$(( (diff % 86400) / 3600 ))" elif [ "$diff" -ge 3600 ]; then printf "in %dh%dm" "$(( diff / 3600 ))" "$(( (diff % 3600) / 60 ))" else printf "in %dm" "$(( diff / 60 ))" fi } # ── Extract JSON data ─────────────────────────────────── model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"') size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000') [ "$size" -eq 0 ] 2>/dev/null && size=200000 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 # Cache hit ratio — shows how efficiently prompts are leveraging Claude's caching. cache_ratio_str="" cache_total=$(( cache_create + cache_read )) if [ "$cache_total" -gt 0 ]; then cache_hit_pct=$(( cache_read * 100 / cache_total )) cache_ratio_str="${dim}cache ${reset}${white}${cache_hit_pct}%${reset}" fi effort="default" settings_path="$HOME/.claude/settings.json" if [ -f "$settings_path" ]; then effort=$(jq -r '.effortLevel // "default"' "$settings_path" 2>/dev/null) fi # Terminal width — used to suppress optional segments on narrow terminals (< 80 cols). term_width=${COLUMNS:-$(tput cols 2>/dev/null || echo 120)} # ── LINE 1: Model │ Context bar+% │ Directory (branch) │ Cache │ Session │ Thinking ── pct_color=$(color_for_pct "$pct_used") cwd=$(echo "$input" | jq -r '.cwd // ""') [ -z "$cwd" ] || [ "$cwd" = "null" ] && cwd=$(pwd) dirname=$(basename "$cwd") git_branch="" git_dirty="" if git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git_branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null) # --no-optional-locks prevents hangs on network drives / repos with lock contention. if [ -n "$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null)" ]; then git_dirty="*" fi fi session_duration="" session_start=$(echo "$input" | jq -r '.session.start_time // empty') if [ -n "$session_start" ] && [ "$session_start" != "null" ]; then start_epoch=$(iso_to_epoch "$session_start") if [ -n "$start_epoch" ]; then now_epoch=$(date +%s) elapsed=$(( now_epoch - start_epoch )) if [ "$elapsed" -ge 3600 ]; then session_duration="$(( elapsed / 3600 ))h$(( (elapsed % 3600) / 60 ))m" elif [ "$elapsed" -ge 60 ]; then session_duration="$(( elapsed / 60 ))m" else session_duration="${elapsed}s" fi fi fi # YOLO mode detection — checks input JSON first, then falls back to parent process args. yolo_mode=false yolo_field=$(echo "$input" | jq -r '.is_unsafe_mode // .dangerously_skip_permissions // false' 2>/dev/null) if [ "$yolo_field" = "true" ]; then yolo_mode=true fi if ! $yolo_mode; then parent_args=$(ps -p $PPID -o args= 2>/dev/null) if echo "$parent_args" | grep -q -- '--dangerously-skip-permissions'; then yolo_mode=true fi fi line1="${blue}${model_name}${reset}" line1+="${sep}" ctx_bar=$(build_bar "$pct_used" 8) line1+="✍️ ${ctx_bar} ${pct_color}${pct_used}% (${used_tokens}/${total_tokens})${reset}" line1+="${sep}" line1+="${cyan}${dirname}${reset}" if [ -n "$git_branch" ]; then line1+=" ${green}(${git_branch}${red}${git_dirty}${green})${reset}" fi if [ "${term_width:-120}" -ge 80 ]; then if [ -n "$cache_ratio_str" ]; then line1+="${sep}" line1+="${cache_ratio_str}" fi if [ -n "$session_duration" ]; then line1+="${sep}" line1+="${dim}⏱ ${reset}${white}${session_duration}${reset}" fi fi line1+="${sep}" case "$effort" in high) line1+="${magenta}● ${effort}${reset}" ;; medium) line1+="${dim}◑ ${effort}${reset}" ;; low) line1+="${dim}◔ ${effort}${reset}" ;; *) line1+="${dim}○ ${effort}${reset}" ;; esac if $yolo_mode; then line1+="${sep}" line1+="${red}⚠ YOLO${reset}" fi # ── OAuth token resolution ────────────────────────────── get_oauth_token() { local token="" if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then echo "$CLAUDE_CODE_OAUTH_TOKEN" return 0 fi 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 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 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 "" } # ── Fetch usage data (cached) ────────────────────────── cache_file="/tmp/claude/statusline-usage-cache.json" cache_max_age=60 mkdir -p /tmp/claude needs_refresh=true usage_data="" 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 if $needs_refresh; then 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 -e '.five_hour' >/dev/null 2>&1; then usage_data="$response" echo "$response" > "$cache_file" fi fi if [ -z "$usage_data" ] && [ -f "$cache_file" ]; then usage_data=$(cat "$cache_file" 2>/dev/null) fi fi # ── Rate limit lines ──────────────────────────────────── rate_lines="" if [ -z "$usage_data" ]; then rate_lines="${dim}⚠ usage data unavailable${reset}" fi if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then bar_width=10 # 2x usage indicator — API signals this via the iguana_necktie field. multiplier_str="" iguana=$(echo "$usage_data" | jq -r '.iguana_necktie // empty' 2>/dev/null) if [ -n "$iguana" ] && [ "$iguana" != "null" ]; then multiplier_str=" ${orange}2x${reset}" fi 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_reset_epoch=$(iso_to_epoch "$five_hour_reset_iso") five_hour_countdown=$(format_countdown "$five_hour_reset_epoch") five_hour_bar=$(build_bar "$five_hour_pct" "$bar_width") five_hour_pct_color=$(color_for_pct "$five_hour_pct") five_hour_pct_fmt=$(printf "%3d" "$five_hour_pct") five_hour_time_str="${white}${five_hour_reset}${reset}" [ -n "$five_hour_countdown" ] && five_hour_time_str="${white}${five_hour_countdown} (${five_hour_reset})${reset}" rate_lines+="${bold}${white}CURRENT${reset}${multiplier_str} ${five_hour_bar} ${five_hour_pct_color}${five_hour_pct_fmt}%${reset} ${dim}⟳${reset} ${five_hour_time_str}" 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_reset_epoch=$(iso_to_epoch "$seven_day_reset_iso") seven_day_countdown=$(format_countdown "$seven_day_reset_epoch") seven_day_bar=$(build_bar "$seven_day_pct" "$bar_width") seven_day_pct_color=$(color_for_pct "$seven_day_pct") seven_day_pct_fmt=$(printf "%3d" "$seven_day_pct") seven_day_time_str="${white}${seven_day_reset}${reset}" [ -n "$seven_day_countdown" ] && seven_day_time_str="${white}${seven_day_countdown} (${seven_day_reset})${reset}" rate_lines+="\n${bold}${white}WEEKLY ${reset} ${seven_day_bar} ${seven_day_pct_color}${seven_day_pct_fmt}%${reset} ${dim}⟳${reset} ${seven_day_time_str}" 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") extra_pct_color=$(color_for_pct "$extra_pct") extra_reset=$(date -v+1m -v1d +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]') if [ -z "$extra_reset" ]; then extra_reset=$(date -d "$(date +%Y-%m-01) +1 month" +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]') fi rate_lines+="\n${bold}${white}EXTRA ${reset} ${extra_bar} ${extra_pct_color}\$${extra_used}${dim}/${reset}${white}\$${extra_limit}${reset} ${dim}resets${reset} ${white}${extra_reset}${reset}" fi fi # ── Output ────────────────────────────────────────────── printf "%b" "$line1" [ -n "$rate_lines" ] && printf "\n\n%b" "$rate_lines" exit 0