Skip to content

Instantly share code, notes, and snippets.

@anatoliykant
Last active March 20, 2026 22:37
Show Gist options
  • Select an option

  • Save anatoliykant/35a3e1e8a1d4f1cef7730fe9f18aa165 to your computer and use it in GitHub Desktop.

Select an option

Save anatoliykant/35a3e1e8a1d4f1cef7730fe9f18aa165 to your computer and use it in GitHub Desktop.
Claude code /statusline from Anatolii
#!/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