Skip to content

Instantly share code, notes, and snippets.

@LaszloMocsy
Last active August 1, 2026 15:37
Show Gist options
  • Select an option

  • Save LaszloMocsy/ba6870a90391ce512d697f36b7aa2304 to your computer and use it in GitHub Desktop.

Select an option

Save LaszloMocsy/ba6870a90391ce512d697f36b7aa2304 to your computer and use it in GitHub Desktop.
Exmaple usage of Claude Code's statusline-command.sh
#!/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