Last active
August 1, 2026 15:37
-
-
Save LaszloMocsy/ba6870a90391ce512d697f36b7aa2304 to your computer and use it in GitHub Desktop.
Exmaple usage of Claude Code's statusline-command.sh
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 | |
| # Claude Code statusLine | |
| # Renders: [model effort] [git] · Ctx <bar> · 5h <bar> · 7d <bar> · +/- · $cost | |
| # Each metric: label [sub-cell bar] NN% <reset-countdown> [pace] | |
| # Bars fill in 1/8th steps; fill = prominent hue, empty = same hue darkened | |
| # (cell background). Truecolor (24-bit) required. | |
| input=$(cat) | |
| # ─── config ────────────────────────────────────────────────────────────── | |
| BAR_WIDTH=10 # cells per bar | |
| WARN_AT=60 # % at which a bar turns amber | |
| CRIT_AT=85 # % at which a bar turns red | |
| GLYPH_RESET='↻' # prefix for reset countdowns | |
| GLYPH_PACE='Δ' # prefix for the pacing delta (text glyph; avoid emoji like ⏱ | |
| # which render double-width/coloured and break alignment) | |
| SEP=' · ' # separator between segments | |
| # Feature toggles (0/1). All default to preserving the original single-line look. | |
| SHOW_MODEL=1 # short model tag, e.g. "Opus" / "Sonnet" | |
| SHOW_EFFORT=1 # reasoning-effort tag next to the model, e.g. "high" / "max" | |
| SHOW_GIT=1 # git branch + dirty marker (only renders inside a repo) | |
| SHOW_LINES=1 # session lines changed, e.g. +342/-88 | |
| SHOW_PACE=1 # burn-rate delta on rate bars: are you ahead of / behind budget? | |
| PACE_MIN=3 # only show the pace delta once |used% - expected%| >= this | |
| # Per-band truecolor triplets: "R;G;B" | |
| # fill (prominent) background (dark) | |
| BAND_OK_FG='40;225;90' | |
| BAND_OK_BG='8;28;14' | |
| BAND_WARN_FG='255;190;30' | |
| BAND_WARN_BG='28;20;4' | |
| BAND_CRIT_FG='255;45;45' | |
| BAND_CRIT_BG='30;6;6' | |
| # Model name colour: Anthropic's signature terracotta (#D97757). | |
| MODEL_FG='217;119;87' | |
| # Accent for the git prefix. | |
| ACCENT_FG='120;170;255' | |
| # Reasoning-effort colours, keyed by the level names Claude Code uses (the API | |
| # set low/medium/high/xhigh/max, plus Claude Code's ultracode + auto). These are | |
| # sampled from Claude Code's own /model effort slider so the statusline matches | |
| # it: gold → green → periwinkle → violet, with red for max. Fully themeable. | |
| EFFORT_LOW_FG='245;195;68' | |
| EFFORT_MEDIUM_FG='108;184;110' | |
| EFFORT_HIGH_FG='178;185;244' | |
| EFFORT_XHIGH_FG='169;137;248' | |
| EFFORT_MAX_FG='235;82;82' | |
| EFFORT_ULTRACODE_FG='235;120;205' | |
| EFFORT_AUTO_FG='140;146;158' | |
| # Optional theme override: put any of the vars above (BAR_WIDTH, BAND_*, glyphs, | |
| # thresholds, SHOW_* toggles, MODEL_FG, EFFORT_*) into this file to recolour or | |
| # reconfigure without editing this script. The preview editor's "Copy" output | |
| # pastes straight in. Overrides win over defaults. | |
| STATUSLINE_THEME="${STATUSLINE_THEME:-$HOME/.claude/statusline-theme.sh}" | |
| [ -f "$STATUSLINE_THEME" ] && source "$STATUSLINE_THEME" | |
| # ───────────────────────────────────────────────────────────────────────── | |
| esc=$'\033' | |
| dim="${esc}[2m" | |
| reset="${esc}[0m" | |
| accent="${esc}[38;2;${ACCENT_FG}m" | |
| model_col="${esc}[38;2;${MODEL_FG}m" | |
| # Full block + left eighth blocks, as explicit UTF-8 bytes (locale-independent). | |
| BLOCK=$'\xe2\x96\x88' | |
| PARTIALS=( | |
| '' | |
| $'\xe2\x96\x8f' $'\xe2\x96\x8e' $'\xe2\x96\x8d' $'\xe2\x96\x8c' | |
| $'\xe2\x96\x8b' $'\xe2\x96\x8a' $'\xe2\x96\x89' | |
| ) | |
| # ─── read fields (single jq pass) ───────────────────────────────────────── | |
| # One jq invocation instead of ~8: statusLine runs on every turn under a tight | |
| # render budget, so we extract everything at once. Each value is emitted on its | |
| # own line and read line-by-line, which preserves empty fields (a tab-delimited | |
| # `read` would collapse adjacent empties and shift every column left). | |
| # | |
| # jq is required. Without it (common on Git Bash / minimal images) every field | |
| # would be empty and you'd get a bare "status unavailable"; say so explicitly. | |
| if ! command -v jq >/dev/null 2>&1; then | |
| printf '%s' "${dim}statusline: jq not found on PATH${reset}" | |
| exit 0 | |
| fi | |
| # `while read` instead of mapfile so this also works on bash 3.2 (macOS default). | |
| F=() | |
| while IFS= read -r _line; do F+=("$_line"); done < <(printf '%s' "$input" | jq -r ' | |
| [ (.context_window.used_percentage // ""), | |
| (.context_window.total_input_tokens // ""), | |
| (.context_window.context_window_size // ""), | |
| (.rate_limits.five_hour.used_percentage // ""), | |
| (.rate_limits.seven_day.used_percentage // ""), | |
| (.rate_limits.five_hour.resets_at // ""), | |
| (.rate_limits.seven_day.resets_at // ""), | |
| (.cost.total_cost_usd // ""), | |
| (.cost.total_lines_added // ""), | |
| (.cost.total_lines_removed // ""), | |
| (.model.display_name // ""), | |
| (.model.id // ""), | |
| (.workspace.current_dir // .cwd // ""), | |
| (.worktree.name // ""), | |
| ((.effort|objects).level // (.effort|strings) | |
| // .model.reasoning_effort // .reasoning_effort // "") | |
| ] | .[]' 2>/dev/null) | |
| used=${F[0]} ctx_tokens=${F[1]} ctx_size=${F[2]} | |
| five=${F[3]} week=${F[4]} five_reset=${F[5]} week_reset=${F[6]} | |
| cost=${F[7]} lines_add=${F[8]} lines_rem=${F[9]} | |
| model_name=${F[10]} model_id=${F[11]} cwd=${F[12]} worktree=${F[13]} | |
| effort_json=${F[14]} # empty today; populated automatically if CC adds the field | |
| # to_epoch <resets_at> -> unix seconds | |
| # Deployed schema sends an integer epoch; tolerate an ISO-8601 string too. | |
| to_epoch() { | |
| local v="$1" | |
| [ -z "$v" ] && return | |
| case "$v" in | |
| *[!0-9]*) date -d "$v" +%s 2>/dev/null || date -jf '%Y-%m-%dT%H:%M:%S%z' \ | |
| "${v/Z/+0000}" +%s 2>/dev/null ;; # GNU first, then BSD/macOS | |
| *) printf '%s' "$v" ;; # already epoch seconds | |
| esac | |
| } | |
| # fmt_reset <resets_at> -> compact "2h14m" / "1d3h" / "8m" / "now" | |
| fmt_reset() { | |
| local ts | |
| ts=$(to_epoch "$1") | |
| [ -z "$ts" ] && return | |
| local now d days hrs mins | |
| now=$(date +%s) | |
| d=$((ts - now)) | |
| [ "$d" -le 0 ] && { | |
| printf 'now' | |
| return | |
| } | |
| days=$((d / 86400)) | |
| hrs=$(((d % 86400) / 3600)) | |
| mins=$(((d % 3600) / 60)) | |
| if [ "$days" -gt 0 ]; then | |
| printf '%dd%dh' "$days" "$hrs" | |
| elif [ "$hrs" -gt 0 ]; then | |
| printf '%dh%dm' "$hrs" "$mins" | |
| else | |
| printf '%dm' "$mins" | |
| fi | |
| } | |
| # fmt_tokens <count> -> compact "78.5k" / "200k" / "512" | |
| fmt_tokens() { | |
| awk -v n="$1" 'BEGIN{ | |
| if (n >= 1000) { v = n / 1000; if (v == int(v)) printf "%dk", v; else printf "%.1fk", v } | |
| else printf "%d", n | |
| }' | |
| } | |
| # pace_delta <used%> <resets_at> <window_seconds> | |
| # Expected consumption at this point in a rolling window is (elapsed/window)%. | |
| # Positive delta = burning hotter than a flat pace (will run out early). | |
| pace_delta() { | |
| local pct="$1" resets_at="$2" win="$3" ts now remaining elapsed expected | |
| ts=$(to_epoch "$resets_at") | |
| [ -z "$ts" ] && return | |
| now=$(date +%s) | |
| remaining=$((ts - now)) | |
| [ "$remaining" -lt 0 ] && remaining=0 | |
| [ "$remaining" -gt "$win" ] && remaining=$win | |
| elapsed=$((win - remaining)) | |
| expected=$((elapsed * 100 / win)) | |
| printf '%d' "$(awk -v u="$pct" -v e="$expected" 'BEGIN{printf "%d", u - e}')" | |
| } | |
| # render_metric <label> <percentage> [resets_at] [extra] [window_seconds] | |
| render_metric() { | |
| local label="$1" pct="$2" resets_at="$3" extra="$4" window="$5" | |
| local p | |
| p=$(printf '%.0f' "$pct") | |
| [ "$p" -lt 0 ] && p=0 | |
| [ "$p" -gt 100 ] && p=100 | |
| local fg bg | |
| if [ "$p" -ge "$CRIT_AT" ]; then | |
| fg="$BAND_CRIT_FG" | |
| bg="$BAND_CRIT_BG" | |
| elif [ "$p" -ge "$WARN_AT" ]; then | |
| fg="$BAND_WARN_FG" | |
| bg="$BAND_WARN_BG" | |
| else | |
| fg="$BAND_OK_FG" | |
| bg="$BAND_OK_BG" | |
| fi | |
| local body="${esc}[38;2;${fg};48;2;${bg}m" # fill on dark bg | |
| local eighths=$(((p * BAR_WIDTH * 8 + 50) / 100)) | |
| local full=$((eighths / 8)) | |
| local rem=$((eighths % 8)) | |
| local used_cells=$((full + (rem > 0 ? 1 : 0))) | |
| local empty=$((BAR_WIDTH - used_cells)) | |
| # Assemble one continuous coloured strip: full blocks, then the partial glyph | |
| # (if any), then empty cells. | |
| local cells="$body" i | |
| for ((i = 0; i < full; i++)); do cells="$cells$BLOCK"; done | |
| [ "$rem" -gt 0 ] && cells="$cells${PARTIALS[$rem]}" | |
| for ((i = 0; i < empty; i++)); do cells="$cells "; done | |
| local bar="${cells}${reset}" | |
| local tail="" | |
| if [ -n "$resets_at" ]; then | |
| local r | |
| r=$(fmt_reset "$resets_at") | |
| [ -n "$r" ] && tail=" ${dim}${GLYPH_RESET}${r}${reset}" | |
| fi | |
| # Pacing delta: ahead of budget (cool, dim) or behind it (hot, warn/crit tint). | |
| local pace="" | |
| if [ "$SHOW_PACE" -eq 1 ] && [ -n "$resets_at" ] && [ -n "$window" ]; then | |
| local d | |
| d=$(pace_delta "$p" "$resets_at" "$window") | |
| if [ -n "$d" ] && [ "${d#-}" -ge "$PACE_MIN" ]; then | |
| if [ "$d" -gt 0 ]; then | |
| pace=" ${esc}[38;2;${BAND_WARN_FG}m${GLYPH_PACE}+${d}${reset}" | |
| else | |
| pace=" ${dim}${GLYPH_PACE}${d}${reset}" | |
| fi | |
| fi | |
| fi | |
| printf '%s' "${label} ${dim}[${reset}${bar}${dim}]${reset} ${dim}${p}%${reset}${extra}${tail}${pace}" | |
| } | |
| # short_model <display_name> <id> -> "Opus" / "Sonnet" / "Haiku" / fallback | |
| short_model() { | |
| local name="$1" id="$2" s="${1}${2}" | |
| case "$s" in | |
| *[Oo]pus*) printf 'Opus' ;; | |
| *[Ss]onnet*) printf 'Sonnet' ;; | |
| *[Hh]aiku*) printf 'Haiku' ;; | |
| *) printf '%s' "${name:-$id}" ;; | |
| esac | |
| } | |
| # resolve_effort -> current effort level, lowercased, or empty. | |
| # Effort is NOT in the statusLine JSON yet (multiple open feature requests), so | |
| # we try, in order: the JSON (future-proof — auto-works if Anthropic adds it), | |
| # then the documented env var, then settings.json. NOTE: a mid-session change | |
| # via the /effort slider or Shift+Tab is not persisted anywhere a script can | |
| # read, so this reflects the configured/launch value and can be stale until the | |
| # JSON field lands. The JSON check above means it upgrades to live on its own. | |
| resolve_effort() { | |
| local v="$effort_json" | |
| [ -z "$v" ] && v="${CLAUDE_CODE_EFFORT_LEVEL:-$CLAUDE_EFFORT}" | |
| if [ -z "$v" ]; then | |
| local f | |
| for f in "$cwd/.claude/settings.json" \ | |
| "${CLAUDE_PROJECT_DIR:-}/.claude/settings.json" \ | |
| "$HOME/.claude/settings.json"; do | |
| [ -n "$f" ] && [ -f "$f" ] || continue | |
| v=$(jq -r '.effortLevel // empty' "$f" 2>/dev/null) | |
| [ -n "$v" ] && break | |
| done | |
| fi | |
| printf '%s' "$v" | tr '[:upper:]' '[:lower:]' # lowercase (bash-3.2-safe) | |
| } | |
| # effort_label -> display text for a level (only ultracode is relabelled). | |
| effort_label() { | |
| case "$1" in | |
| ultracode) printf 'ultra' ;; | |
| *) printf '%s' "$1" ;; | |
| esac | |
| } | |
| # effort_segment -> coloured effort label, e.g. a green "medium" or red "max". | |
| effort_segment() { | |
| local lvl | |
| lvl=$(resolve_effort) | |
| [ -z "$lvl" ] && return | |
| local label upper varname col | |
| label=$(effort_label "$lvl") | |
| upper=$(printf '%s' "$lvl" | tr '[:lower:]' '[:upper:]') | |
| varname="EFFORT_${upper}_FG" | |
| col=${!varname:-} | |
| [ -z "$col" ] && col="$ACCENT_FG" # unknown level: fall back to accent | |
| printf '%s' "${esc}[38;2;${col}m${label}${reset}" | |
| } | |
| # git_segment -> "branch✱" for the repo at $cwd, empty if not a repo | |
| git_segment() { | |
| [ -z "$cwd" ] && return | |
| command -v git >/dev/null 2>&1 || return | |
| local br | |
| br=$(git -C "$cwd" symbolic-ref --quiet --short HEAD 2>/dev/null) || | |
| br=$(git -C "$cwd" rev-parse --short HEAD 2>/dev/null) || return | |
| local dirty="" | |
| git -C "$cwd" diff --quiet --ignore-submodules HEAD 2>/dev/null || dirty='✱' | |
| printf '%s%s' "$br" "$dirty" | |
| } | |
| # ─── assemble line ─────────────────────────────────────────────────────── | |
| parts=() | |
| # Model tag (+ effort) as one segment; git branch as its own segment so the | |
| # usual ' · ' separator falls between the model and the branch. | |
| prefix="" | |
| if [ "$SHOW_MODEL" -eq 1 ] && { [ -n "$model_name" ] || [ -n "$model_id" ]; }; then | |
| prefix="${model_col}$(short_model "$model_name" "$model_id")${reset}" | |
| fi | |
| if [ "$SHOW_EFFORT" -eq 1 ]; then | |
| e=$(effort_segment) | |
| [ -n "$e" ] && prefix="${prefix:+$prefix }${e}" | |
| fi | |
| [ -n "$prefix" ] && parts+=("$prefix") | |
| if [ "$SHOW_GIT" -eq 1 ]; then | |
| g=$(git_segment) | |
| [ -n "$g" ] && parts+=("${dim}⎇ ${reset}${accent}${g}${reset}") | |
| fi | |
| ctx_tail="" | |
| if [ -n "$ctx_tokens" ] && [ -n "$ctx_size" ]; then | |
| ctx_remaining=$((ctx_size - ctx_tokens)) | |
| [ "$ctx_remaining" -lt 0 ] && ctx_remaining=0 | |
| ctx_tail=" ${dim}$(fmt_tokens "$ctx_tokens") / $(fmt_tokens "$ctx_remaining") left${reset}" | |
| fi | |
| [ -n "$used" ] && parts+=("$(render_metric 'Ctx' "$used" '' "$ctx_tail")") | |
| [ -n "$five" ] && parts+=("$(render_metric '5h' "$five" "$five_reset" '' 18000)") | |
| [ -n "$week" ] && parts+=("$(render_metric '7d' "$week" "$week_reset" '' 604800)") | |
| # Session lines changed (+added / -removed). | |
| if [ "$SHOW_LINES" -eq 1 ] && { [ -n "$lines_add" ] || [ -n "$lines_rem" ]; }; then | |
| la=${lines_add:-0} | |
| lr=${lines_rem:-0} | |
| if [ "$la" -gt 0 ] || [ "$lr" -gt 0 ]; then | |
| parts+=("$(printf '%s' "${dim}${esc}[38;2;${BAND_OK_FG}m+${la}${reset}${dim}/${esc}[38;2;${BAND_CRIT_FG}m-${lr}${reset}")") | |
| fi | |
| fi | |
| if [ -n "$cost" ]; then | |
| parts+=("$(printf '%s' "${dim}\$$(printf '%.2f' "$cost")${reset}")") | |
| fi | |
| if [ ${#parts[@]} -eq 0 ]; then | |
| # Reaching here means jq ran (guarded above) but produced no usable fields. | |
| # Usually a malformed/empty stdin payload rather than a config problem. | |
| if [ -z "$input" ] || ! printf '%s' "$input" | jq -e . >/dev/null 2>&1; then | |
| printf '%s' "${dim}statusline: no valid JSON on stdin${reset}" | |
| else | |
| printf '%s' "${dim}status unavailable${reset}" | |
| fi | |
| else | |
| out="" | |
| for i in "${!parts[@]}"; do | |
| [ "$i" -gt 0 ] && out="$out${dim}${SEP}${reset}" | |
| out="$out${parts[$i]}" | |
| done | |
| printf '%s' "$out" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment