Last active
March 20, 2026 22:37
-
-
Save anatoliykant/35a3e1e8a1d4f1cef7730fe9f18aa165 to your computer and use it in GitHub Desktop.
Claude code /statusline from Anatolii
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 | |
| # Claude Code status line β colorful 3-line layout | |
| # Line 1: Model/thinking/fast | context bar | usage | |
| # Line 2: Git branch | timer | directory | |
| # Line 3: iTerm2 tab bar β Claude Code sessions with status + clickable links | |
| # Reads JSON from stdin, fetches usage from Anthropic API (cached) | |
| input=$(cat) | |
| # ββ ANSI Colors ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| RST='\033[0m' | |
| DIM='\033[2m' | |
| GREEN='\033[32m' | |
| YELLOW='\033[33m' | |
| BLUE='\033[34m' | |
| CYAN='\033[36m' | |
| GRAY='\033[90m' | |
| BGREEN='\033[1;32m' | |
| BYELLOW='\033[1;33m' | |
| BRED='\033[1;31m' | |
| BCYAN='\033[1;36m' | |
| # ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| get() { echo "$input" | jq -r "$1 // empty" 2>/dev/null; } | |
| color_pct() { | |
| local pct="$1" | |
| if [ -z "$pct" ] || [ "$pct" = "--" ]; then | |
| printf "${GRAY}--%${RST}"; return | |
| fi | |
| if [ "$pct" -lt 50 ] 2>/dev/null; then | |
| printf "${GREEN}${pct}%%${RST}" | |
| elif [ "$pct" -lt 80 ] 2>/dev/null; then | |
| printf "${YELLOW}${pct}%%${RST}" | |
| else | |
| printf "${BRED}${pct}%%${RST}" | |
| fi | |
| } | |
| # ββ Model + Thinking + Fast ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| model_name=$(get '.model.display_name') | |
| [ -z "$model_name" ] && model_name=$(get '.model.id') | |
| [ -z "$model_name" ] && model_name="--" | |
| model_short=$(echo "$model_name" | sed 's/ [0-9].*$//') | |
| STRIKE='\033[9m' | |
| thinking_str="" | |
| fast_str="" | |
| # ββ Read alwaysThinkingEnabled + fastMode from settings files βββββββββββββββββ | |
| # Priority: project settings > global settings | |
| # (claude config get is NOT used β it can hang or fail in statusline context) | |
| _read_setting() { | |
| local key="$1" | |
| local val="" | |
| # Try project-level settings first (cwd from JSON input) | |
| local proj_dir | |
| proj_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // empty' 2>/dev/null) | |
| if [ -n "$proj_dir" ] && [ -f "${proj_dir}/.claude/settings.json" ]; then | |
| val=$(jq -r ".${key} // empty" "${proj_dir}/.claude/settings.json" 2>/dev/null | tr -d '[:space:]') | |
| fi | |
| # Fall back to global settings | |
| if [ -z "$val" ] && [ -f "$HOME/.claude/settings.json" ]; then | |
| val=$(jq -r ".${key} // empty" "$HOME/.claude/settings.json" 2>/dev/null | tr -d '[:space:]') | |
| fi | |
| echo "$val" | |
| } | |
| te=$(_read_setting "alwaysThinkingEnabled") | |
| [ "$te" != "true" ] && te="false" | |
| fm=$(_read_setting "fastMode") | |
| [ "$fm" != "true" ] && fm="false" | |
| # ββ Thinking indicator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # π§ thinking only when alwaysThinkingEnabled=true AND fastMode=false. | |
| # All other combinations: π + strikethrough "thinking" | |
| if [ "$te" = "true" ] && [ "$fm" != "true" ]; then | |
| thinking_str=" π§ thinking" | |
| else | |
| thinking_str=" π${STRIKE}thinking${RST}" | |
| fi | |
| # ββ Fast mode indicator βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if [ "$fm" = "true" ]; then | |
| fast_str=" β‘οΈfast" | |
| else | |
| fast_str=" π’${STRIKE}fast${RST}" | |
| fi | |
| model_part="${BCYAN}${model_short}${RST}${DIM}${thinking_str}${RST}${fast_str}" | |
| # ββ Context Progress Bar (10 chars, colored) βββββββββββββββββββββββββββββββββ | |
| context_size=$(get '.context_window.context_window_size') | |
| used_pct=$(get '.context_window.used_percentage') | |
| # Use current context input tokens (matches the progress bar) if available, | |
| # fall back to total_input_tokens for sessions without a current API call yet | |
| current_input=$(get '.context_window.current_usage.input_tokens') | |
| total_input=$(get '.context_window.total_input_tokens') | |
| [ -n "$current_input" ] && [ "$current_input" != "0" ] && total_input="$current_input" | |
| BAR_W=10 | |
| if [ -n "$used_pct" ] && [ "$used_pct" != "null" ]; then | |
| pct_int=$(printf "%.0f" "$used_pct") | |
| filled=$((pct_int * BAR_W / 100)) | |
| # At least 1 filled block when usage > 0 | |
| [ "$pct_int" -gt 0 ] && [ "$filled" -lt 1 ] && filled=1 | |
| [ "$filled" -gt "$BAR_W" ] && filled=$BAR_W | |
| [ "$filled" -lt 0 ] && filled=0 | |
| empty=$((BAR_W - filled)) | |
| if [ "$pct_int" -lt 50 ]; then | |
| BC=$GREEN | |
| elif [ "$pct_int" -lt 75 ]; then | |
| BC=$YELLOW | |
| else | |
| BC=$BRED | |
| fi | |
| bar_f=""; bar_e="" | |
| i=0; while [ $i -lt $filled ]; do bar_f="${bar_f}β"; i=$((i+1)); done | |
| i=0; while [ $i -lt $empty ]; do bar_e="${bar_e}β"; i=$((i+1)); done | |
| # Compute used tokens from used_percentage Γ context_window_size so both numbers are always consistent | |
| used_tokens=$(echo "$used_pct $context_size" | awk '{printf "%.0f", $1/100*$2}') | |
| used_k=$(echo "$used_tokens" | awk '{if($1>=1000000)printf "%.1fM",$1/1000000;else if($1==0)printf "0";else if($1<500)printf "<1k";else printf "%.0fk",$1/1000}') | |
| ctx_k=$(echo "$context_size" | awk '{if($1>=1000000)printf "%.0fM",$1/1000000;else printf "%.0fk",$1/1000}') | |
| bar_part="[${BC}${bar_f}${GRAY}${bar_e}${RST}] ${used_k}/${ctx_k} (${pct_int}%)" | |
| else | |
| # New session β no messages yet, show zeroed-out bar | |
| empty_bar="ββββββββββ" | |
| ctx_k="--" | |
| if [ -n "$context_size" ] && [ "$context_size" != "null" ]; then | |
| ctx_k=$(echo "$context_size" | awk '{if($1>=1000000)printf "%.0fM",$1/1000000;else printf "%.0fk",$1/1000}') | |
| fi | |
| bar_part="[${GRAY}${empty_bar}${RST}] ${GREEN}0${RST}/${ctx_k} (${GREEN}0%${RST})" | |
| fi | |
| # ββ Usage: Session / Weekly (Anthropic API with cache) βββββββββββββββββββββββ | |
| CACHE_DIR="$HOME/.cache/claude-statusline" | |
| CACHE_FILE="$CACHE_DIR/usage.json" | |
| CACHE_MAX_AGE=180 | |
| LOCK_FILE="$CACHE_DIR/usage.lock" | |
| session_pct="--" | |
| weekly_pct="--" | |
| parse_cached() { | |
| if [ -f "$CACHE_FILE" ]; then | |
| local s w | |
| s=$(jq -r '.sessionUsage // empty' "$CACHE_FILE" 2>/dev/null) | |
| w=$(jq -r '.weeklyUsage // empty' "$CACHE_FILE" 2>/dev/null) | |
| [ -n "$s" ] && session_pct=$(echo "$s" | awk '{printf "%.0f", $1}') | |
| [ -n "$w" ] && weekly_pct=$(echo "$w" | awk '{printf "%.0f", $1}') | |
| fi | |
| } | |
| fetch_usage_bg() { | |
| ( | |
| mkdir -p "$CACHE_DIR" | |
| # Get token from macOS keychain | |
| token_json=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) | |
| [ -z "$token_json" ] && exit 0 | |
| token=$(echo "$token_json" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| [ -z "$token" ] && exit 0 | |
| resp=$(curl -s --max-time 5 \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| "https://api.anthropic.com/api/oauth/usage" 2>/dev/null) | |
| [ -z "$resp" ] && exit 0 | |
| echo "$resp" | jq '{ | |
| sessionUsage: .five_hour.utilization, | |
| sessionResetAt: .five_hour.resets_at, | |
| weeklyUsage: .seven_day.utilization, | |
| weeklyResetAt: .seven_day.resets_at | |
| }' > "$CACHE_FILE" 2>/dev/null | |
| rm -f "$LOCK_FILE" | |
| ) &>/dev/null & | |
| } | |
| # Always try to read cache first (instant) | |
| parse_cached | |
| # Invalidate cache if a reset time has passed (prevents showing stale pre-reset values) | |
| cache_expired=false | |
| if [ -f "$CACHE_FILE" ]; then | |
| now_epoch=$(date +%s) | |
| for reset_key in sessionResetAt weeklyResetAt; do | |
| reset_ts=$(jq -r ".${reset_key} // empty" "$CACHE_FILE" 2>/dev/null) | |
| if [ -n "$reset_ts" ]; then | |
| reset_epoch=$(date -jf "%Y-%m-%dT%H:%M:%S" "$(echo "$reset_ts" | cut -c1-19)" +%s 2>/dev/null || echo 0) | |
| cache_mtime=$(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) | |
| # Cache was written before the reset, but reset has already happened | |
| if [ "$cache_mtime" -lt "$reset_epoch" ] && [ "$now_epoch" -ge "$reset_epoch" ]; then | |
| cache_expired=true | |
| break | |
| fi | |
| fi | |
| done | |
| fi | |
| # Refresh in background if cache is stale or expired past a reset boundary | |
| if [ -f "$CACHE_FILE" ]; then | |
| cache_age=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) )) | |
| if { [ "$cache_age" -ge "$CACHE_MAX_AGE" ] || [ "$cache_expired" = true ]; } && [ ! -f "$LOCK_FILE" ]; then | |
| touch "$LOCK_FILE" 2>/dev/null | |
| fetch_usage_bg | |
| fi | |
| else | |
| # No cache yet β trigger first fetch | |
| mkdir -p "$CACHE_DIR" | |
| if [ ! -f "$LOCK_FILE" ]; then | |
| touch "$LOCK_FILE" 2>/dev/null | |
| fetch_usage_bg | |
| fi | |
| fi | |
| session_colored=$(color_pct "$session_pct") | |
| weekly_colored=$(color_pct "$weekly_pct") | |
| # ββ Session Timer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| duration_ms=$(get '.cost.total_duration_ms') | |
| if [ -n "$duration_ms" ] && [ "$duration_ms" != "null" ] && [ "$duration_ms" != "0" ]; then | |
| total_sec=$(echo "$duration_ms" | awk '{printf "%.0f", $1/1000}') | |
| mins=$((total_sec / 60)) | |
| secs=$((total_sec % 60)) | |
| else | |
| total_sec=0 | |
| mins=0 | |
| secs=0 | |
| fi | |
| # ββ Clock emoji based on elapsed session time ββββββββββββββββββββββββββββββββ | |
| # Use transcript file creation time to determine session age in seconds | |
| transcript_path=$(get '.transcript_path') | |
| elapsed_sec=0 | |
| if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then | |
| created=$(stat -f %B "$transcript_path" 2>/dev/null) | |
| if [ -n "$created" ] && [ "$created" != "0" ]; then | |
| now=$(date +%s) | |
| elapsed_sec=$((now - created)) | |
| [ "$elapsed_sec" -lt 0 ] && elapsed_sec=0 | |
| fi | |
| fi | |
| # Map elapsed_sec to clock emoji (half-hour steps, 12-hour cycle) | |
| elapsed_min=$((elapsed_sec / 60)) | |
| elapsed_h=$((elapsed_min / 60)) | |
| elapsed_m=$((elapsed_min % 60)) | |
| if [ "$elapsed_h" -ge 12 ]; then | |
| clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 11 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π¦" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 10 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π₯" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 9 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π€" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 8 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π£" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 7 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π’" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 6 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π‘" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 5 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π " || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 4 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 3 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 2 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π" || clock_emoji="π" | |
| elif [ "$elapsed_h" -eq 1 ]; then | |
| [ "$elapsed_m" -ge 30 ] && clock_emoji="π" || clock_emoji="π" | |
| else | |
| clock_emoji="π" | |
| fi | |
| # Format timer: seconds only, or Xm Ys, or Xh Ym, or Xd Yh | |
| if [ "$mins" -ge 1440 ] 2>/dev/null; then | |
| days=$((mins / 1440)) | |
| hours=$(( (mins % 1440) / 60 )) | |
| timer_part="${clock_emoji} ${days}d ${hours}h" | |
| elif [ "$mins" -ge 60 ] 2>/dev/null; then | |
| hours=$((mins / 60)) | |
| rem_mins=$((mins % 60)) | |
| timer_part="${clock_emoji} ${hours}h ${rem_mins}m" | |
| elif [ "$mins" -gt 0 ] 2>/dev/null; then | |
| timer_part="${clock_emoji} ${mins}m ${secs}s" | |
| else | |
| timer_part="${clock_emoji} ${secs}s" | |
| fi | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LINE 1: Model thinkingβ‘fast [bar] tokens | Session: N% | Weekly: N% | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| line1="${model_part} ${bar_part} | Session: ${session_colored} | Weekly: ${weekly_colored}" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LINE 2: πΏ branch +N~N | β± Nm Ns | π dir | π project | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| cwd=$(get '.workspace.current_dir') | |
| [ -z "$cwd" ] && cwd=$(get '.cwd') | |
| # ββ Git Branch + Changes ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| git_branch="" | |
| if [ -n "$cwd" ] && { [ -d "$cwd/.git" ] || 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 || git -C "$cwd" rev-parse --short HEAD 2>/dev/null) | |
| fi | |
| git_part="" | |
| if [ -n "$git_branch" ]; then | |
| added=0; removed=0 | |
| stats=$(git -C "$cwd" diff --no-lock-index --shortstat HEAD 2>/dev/null) | |
| if [ -n "$stats" ]; then | |
| a=$(echo "$stats" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+') | |
| r=$(echo "$stats" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+') | |
| [ -n "$a" ] && added=$a | |
| [ -n "$r" ] && removed=$r | |
| fi | |
| changes="" | |
| [ "$added" -gt 0 ] 2>/dev/null && changes="${BGREEN}+${added}${RST}" | |
| [ "$removed" -gt 0 ] 2>/dev/null && changes="${changes}${BYELLOW}~${removed}${RST}" | |
| git_part="πΏ ${GREEN}${git_branch}${RST}" | |
| [ -n "$changes" ] && git_part="${git_part} ${changes}" | |
| else | |
| git_part="πΏ ${GRAY}(no git)${RST}" | |
| fi | |
| # ββ Directory (short path, bright project name) βββββββββββββββββββββββββββββ | |
| dir_short="${cwd/#$HOME/~}" | |
| dir_base=$(dirname "$dir_short") | |
| dir_name=$(basename "$dir_short") | |
| if [ "$dir_base" = "." ]; then | |
| dir_part="π ${BCYAN}${dir_name}${RST}" | |
| else | |
| dir_part="π ${CYAN}${dir_base}/${BCYAN}${dir_name}${RST}" | |
| fi | |
| # ββ GitHub Branch Link (make branch name a clickable link to /pulls) ββββββββ | |
| if [ -n "$cwd" ] && [ -n "$git_branch" ]; then | |
| remote_url=$(git -C "$cwd" --no-optional-locks remote get-url origin 2>/dev/null) | |
| repo_slug="" | |
| if [ -n "$remote_url" ]; then | |
| repo_slug=$(echo "$remote_url" | sed -E \ | |
| -e 's#^git@github\.com:##' \ | |
| -e 's#^https?://github\.com/##' \ | |
| -e 's#\.git$##') | |
| owner=$(echo "$repo_slug" | cut -d/ -f1) | |
| repo=$(echo "$repo_slug" | cut -d/ -f2) | |
| # Validate: owner and repo must be different (i.e. slug contains a slash) | |
| [ -z "$owner" ] || [ -z "$repo" ] || [ "$owner" = "$repo_slug" ] && repo_slug="" | |
| fi | |
| if [ -n "$repo_slug" ]; then | |
| gh_url="https://github.com/${repo_slug}" | |
| git_part="πΏ \033]8;;${gh_url}/pulls\a${GREEN}${git_branch}${RST}\033]8;;\a" | |
| [ -n "$changes" ] && git_part="${git_part} ${changes}" | |
| fi | |
| fi | |
| line2="${git_part} ${timer_part} | ${dir_part}" | |
| # ββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| printf '%b\n%b\n' "$line1" "$line2" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment