Skip to content

Instantly share code, notes, and snippets.

@greglamb
Last active August 7, 2026 02:43
Show Gist options
  • Select an option

  • Save greglamb/26ee6f961ba6bd7a436d745adb99657b to your computer and use it in GitHub Desktop.

Select an option

Save greglamb/26ee6f961ba6bd7a436d745adb99657b to your computer and use it in GitHub Desktop.
Claude Code Status Bar

Emoji Legend

โฐ Current Time

๐Ÿค– Current Model

๐Ÿ’ช Effort Level

๐Ÿง  Model Context

๐Ÿง˜ Optimum Context

โ™ป๏ธ Auto Compact Context

โฒ๏ธ 5h Limit (Usage & Reset)

๐Ÿ 5h Reset

๐Ÿ“… 7d Limit (Usage & Reset)

๐Ÿณ๏ธ 7d Reset

๐Ÿ”ฅ Token Burn Rate

โณ Warming Up (Insufficient Data)

๐ŸŸข Healthy

โš ๏ธ Warning

๐Ÿšจ Alert

๐Ÿ–ฅ๏ธ Machine (Hostname)

๐Ÿ“ Directory

๐Ÿ”’ Sandbox

๐Ÿ‹ Container

๐ŸŒฟ Git Branch

โžก๏ธ Upstream Branch

โฌ†๏ธ Commits Ahead

โฌ‡๏ธ Commits Behind

๐Ÿ“ฅ Staged Changes

๐Ÿ“ Modified Files

๐Ÿ’ฅ Merge Conflicts

๐Ÿ†• Untracked Files

โœ… Clean Working Tree

๐Ÿ”€ Pull Request

โœ“ PR Approved

โœ— PR Changes Requested

โ—‹ PR Review Pending

โ—‡ PR Draft

๐Ÿ”Œ Plugged In

๐Ÿ”‹ On Battery

๐Ÿชซ Low Battery

โŒจ๏ธ Keybinding Hints

Example

  ๐Ÿ–ฅ๏ธ mymac | ๐Ÿ“ ~/Repositories/project | ๐ŸŒฟ main โžก๏ธ origin/main โœ… clean | ๐Ÿ”€ PR#123 โœ“
  ๐Ÿค– Fable 5 (1M context) โ‹ฎ ๐Ÿ’ช xhigh | ๐Ÿง  230k/1M (23%) โ‹ฎ ๐Ÿง˜ 230k/400k (57%) โš ๏ธ โ‹ฎ Fable: 61% โš ๏ธ / Snt: 12% ๐ŸŸข | โ™ป๏ธ 230k/950k (24%) ๐ŸŸข | ๐Ÿ”Œ plugged in ๐ŸŸข
  โฐ 1:45 PM โ‹ฎ ๐Ÿ 5:08 PM โฒ๏ธ โ‹ฎ ๐Ÿณ๏ธ Mon 7/20 ๐Ÿ“… | โฒ๏ธ 42% (3h 23m) ๐ŸŸข โ‹ฎ ๐Ÿ”ฅ 26%/h (2h 13m) ๐Ÿšจ | ๐Ÿ“… 62% (2d 20h 47m) โš ๏ธ โ‹ฎ ๐Ÿ”ฅ 14%/d avg (2d 12h 48m) โš ๏ธ
  โŒจ๏ธ Control+U: Clear | Hold Space: Voice
{
"attribution": {
"sessionUrl": false
},
"cleanupPeriodDays": 3650,
"effortLevel": "xhigh",
"env": {
"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1"
},
"showClearContextOnPlanAccept": false,
"statusLine": {
"command": "~/.claude/statusline.sh",
"padding": 0,
"type": "command"
},
"tui": "fullscreen",
"voice": {
"enabled": true,
"mode": "hold"
}
}
#!/bin/bash
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Claude Code โ€” status line (three rows)
#
# Row 1: ๐Ÿ–ฅ๏ธ/๐Ÿ”’/๐Ÿ‹ hostname | ๐Ÿ“ path | ๐ŸŒฟ git branch, upstream, ahead/behind, working-tree state | ๐Ÿ”€ PR
# Row 2: ๐Ÿค– model โ‹ฎ ๐Ÿ’ช effort | ๐Ÿง  context โ‹ฎ ๐Ÿง˜ memory | โ™ป๏ธ autocompact | ๐Ÿ”‹ battery
# Row 3: โฐ clock โ‹ฎ โฒ๏ธ 5h reset โ‹ฎ ๐Ÿ“… 7d reset date | โฒ๏ธ 5h quota โ‹ฎ ๐Ÿ”ฅ burn | ๐Ÿ“… 7d quota โ‹ฎ ๐Ÿ”ฅ burn โ‹ฎ per-model
# (โฒ๏ธ = the 5-hour window, ๐Ÿ“… = the 7-day window โ€” each shown as a reset time and as usage%)
# Row 4: โŒจ๏ธ static keybinding hints, e.g. "Control+U: Clear | Hold Space: Voice" (SHOW_HINTS / HINTS_TEXT)
#
# Dependencies: /bin/bash 3.2 (macOS stock), jq, git; curl only for the
# per-model quotas segment.
# Data sources:
# 1. stdin JSON from Claude Code (always fresh, no network); effort level
# prefers the native stdin field and falls back to settings.json.
# 2. https://api.anthropic.com/api/oauth/usage โ€” UNDOCUMENTED community-
# discovered endpoint: per-model weekly quotas, plus account-fresh 5h/7d
# percentages so another session's burn is never understated here.
# Refreshed in the background (never blocks a render), cached, mkdir-locked.
# 3. usage-history.log โ€” shared sample log powering sliding-window burn
# rates (true current pace across sessions); prunes itself past 8 days.
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
MEM_LIMIT=400000 # ๐Ÿง˜ memory budget (tokens)
SETTINGS_FILE="$HOME/.claude/settings.json"
# Toggles and paths (env-overridable: "SHOW_PR=0 ~/.claude/statusline.sh")
: "${SHOW_PR:=1}" # ๐Ÿ”€ PR number + review state on row 1
: "${SHOW_WEEK_DATE:=1}" # ๐Ÿ“… 7-day reset date next to the โฒ๏ธ 5h reset
: "${SHOW_WEEKLY_MODELS:=1}" # per-model 7-day quotas appended to ๐Ÿง˜ memory on row 2
: "${SHOW_PIPE_STYLE:=1}" # bold bright-blue | separators; muted-blue โ‹ฎ sub-separators
: "${PIPE_FG:=39}" # 256-color foreground for the | glyph (bright blue)
: "${PIPE_BG:=0}" # 256-color background for the | glyph (black)
: "${SUBSEP_FG:=189}" # 256-color fg for the โ‹ฎ glyph (near-white, faint blue); "none" = leave the terminal default (exact font match)
: "${SHOW_HINTS:=1}" # 4th row: static keybinding hints
# HINTS_TEXT keyboard shortcuts follow the Google developer-docs style: spell
# out modifier keys (Control, not Ctrl/^), join with +, uppercase letter keys,
# and spell out confusing chars (comma, hyphen, period, plus).
: "${HINTS_TEXT:=โŒจ๏ธ Control+U: Clear | Hold Space: Voice}"
# usage-API refresh + account-fresh quota merge. Defaults off inside a Docker
# Sandbox: there, .credentials.json holds a proxy-managed sentinel (a short,
# format-shaped placeholder) rather than a usable token โ€” the real credential is
# injected host-side by the sandbox proxy and never enters the VM โ€” so the API
# can never authenticate and the refresh is a doomed curl every REFRESH_INTERVAL.
# Nothing is lost: stdin still carries real 5h/7d percentages, and
# weekly_models_part renders from whatever the host left in USAGE_CACHE (mount
# it into the sandbox to keep that row alive). An explicit env value still wins.
if [ -n "$IS_SANDBOX" ]; then
: "${USE_USAGE_API:=0}"
else
: "${USE_USAGE_API:=1}"
fi
: "${REFRESH_INTERVAL:=60}" # seconds between usage-API refreshes
: "${USAGE_CACHE:=$HOME/.claude/usage-cache.json}"
: "${HISTORY_FILE:=$HOME/.claude/usage-history.log}"
CREDENTIALS_FILE="$HOME/.claude/.credentials.json"
KEYCHAIN_SERVICE="Claude Code-credentials" # macOS Keychain entry holding the OAuth blob
LOCK_DIR="$HOME/.claude/statusline-refresh.lockdir"
# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
fmt_dur() {
local s=$1
local d=$((s / 86400)) h=$(((s % 86400) / 3600)) m=$(((s % 3600) / 60)) out=""
[ "$d" -gt 0 ] && out="${d}d "
[ "$h" -gt 0 ] && out="${out}${h}h "
[ "$m" -gt 0 ] && out="${out}${m}m"
out="${out% }"
[ -z "$out" ] && out="<1m"
echo "$out"
}
# Control bytes in untrusted strings (branch/model names) could carry terminal
# escape sequences โ€” strip them before display.
sanitize() {
printf '%s' "$1" | tr -d '[:cntrl:]'
}
# ๐ŸŸข/โš ๏ธ/๐Ÿšจ for a percentage at the standard 50/80 thresholds.
usage_dot() {
if [ "$1" -ge 80 ]; then echo "๐Ÿšจ"
elif [ "$1" -ge 50 ]; then echo "โš ๏ธ"
else echo "๐ŸŸข"
fi
}
# ISO 8601 โ†’ epoch. BSD date first (macOS), GNU fallback. The usage API
# returns +00:00 offsets, so parsing the stripped core as UTC is correct;
# other offsets are left to the GNU path.
iso_to_epoch() {
local iso="$1" core
core="${iso%%.*}"
core="${core%%+*}"
core="${core%Z}"
date -juf "%Y-%m-%dT%H:%M:%S" "$core" +%s 2>/dev/null && return
date -d "$iso" +%s 2>/dev/null
}
# merge_quota <stdin_pct> <stdin_reset> <cache_pct> <cache_reset>
# Echoes "pct|reset". stdin reflects this session's last API response; the
# cache can be fresher when another session is doing the burning. A cache
# from a newer window wins outright; the same window (resets within 120s)
# takes the higher percentage โ€” usage within a window only grows.
merge_quota() {
local sp="$1" sr="$2" cp="$3" cr="$4" d spi cpi
if [ -z "$cp" ] || [ -z "$cr" ]; then echo "${sp}|${sr}"; return; fi
if [ -z "$sp" ] || [ -z "$sr" ]; then echo "${cp}|${cr}"; return; fi
d=$((cr - sr))
if [ "$d" -gt 120 ]; then
echo "${cp}|${cr}"
elif [ "$d" -lt -120 ]; then
echo "${sp}|${sr}"
else
spi=$(printf '%.0f' "$sp" 2>/dev/null)
cpi=$(printf '%.0f' "$cp" 2>/dev/null)
if [ "${cpi:-0}" -gt "${spi:-0}" ]; then
echo "${cp}|${sr}"
else
echo "${sp}|${sr}"
fi
fi
}
# quota_segment <emoji> <raw_pct> <reset_epoch> <window> <min_burn_elapsed>
# <burn_unit> <burn_suffix> <burn_warn> <burn_crit> <min_data_label>
# <hist_lookback> <hist_min_span> <hist_pct_col> <hist_reset_col>
# Renders "emoji pct% (time-left) dot [โ‹ฎ ๐Ÿ”ฅ rate (eta) dot]". The burn rate
# prefers a sliding window over the shared sample history โ€” the true current
# pace, counting every session โ€” and falls back to the whole-window average
# until enough history spans hist_min_span. A reset moment already in the
# past means the window rolled over, so the stale % is really 0.
quota_segment() {
local emoji="$1" raw="$2" reset="$3" window="$4" min_elapsed="$5"
local unit="$6" suffix="$7" warn="$8" crit="$9" min_label="${10}"
local lb="${11}" min_span="${12}" pcol="${13}" rcol="${14}"
local pct left="--" diff=0 dot burn="" elapsed rate eta efmt edot
local sample ts0 p0 span dpct
pct=$(printf '%.0f' "$raw" 2>/dev/null) || pct=0
if [ -n "$reset" ]; then
diff=$((reset - NOW))
if [ "$diff" -le 0 ]; then diff=0; pct=0; fi
left=$(fmt_dur $diff)
fi
dot=$(usage_dot "$pct")
# Sliding-window burn: oldest history sample within the lookback that
# belongs to the same quota window (reset within 120s).
if [ -n "$lb" ] && [ -n "$reset" ] && [ -f "$HISTORY_FILE" ] \
&& [ "$pct" -gt 0 ] && [ "$pct" -lt 100 ]; then
sample=$(awk -v now="$NOW" -v lb="$lb" -v pc="$pcol" -v rc="$rcol" -v rs="$reset" \
'$1+0 >= now-lb && $pc != "-" && $rc != "-" && ($rc-rs) <= 120 && ($rc-rs) >= -120 { print $1, $pc; exit }' \
"$HISTORY_FILE" 2>/dev/null)
if [ -n "$sample" ]; then
ts0=${sample%% *}
p0=${sample##* }
span=$((NOW - ts0))
dpct=$((pct - p0))
if [ "$span" -ge "$min_span" ] && [ "$dpct" -ge 0 ]; then
rate=$((dpct * unit / span))
if [ "$dpct" -gt 0 ]; then
efmt=$(fmt_dur $(((100 - pct) * span / dpct)))
else
efmt="--"
fi
if [ "$rate" -ge "$crit" ]; then edot="๐Ÿšจ"
elif [ "$rate" -ge "$warn" ]; then edot="โš ๏ธ"
else edot="๐ŸŸข"
fi
burn=" โ‹ฎ ๐Ÿ”ฅ ${rate}${suffix} (${efmt}) ${edot}"
fi
fi
fi
# Fallback: average since the window opened (the original behavior).
elapsed=$((window - diff))
if [ -z "$burn" ] && [ "$pct" -gt 0 ] && [ "$pct" -lt 100 ] && [ "$elapsed" -gt "$min_elapsed" ]; then
rate=$((pct * unit / elapsed))
eta=$(((100 - pct) * elapsed / pct))
efmt=$(fmt_dur $eta)
if [ "$rate" -ge "$crit" ]; then edot="๐Ÿšจ"
elif [ "$rate" -ge "$warn" ]; then edot="โš ๏ธ"
else edot="๐ŸŸข"
fi
burn=" โ‹ฎ ๐Ÿ”ฅ ${rate}${suffix} (${efmt}) ${edot}"
elif [ -z "$burn" ] && [ "$pct" -gt 0 ] && [ "$pct" -lt 100 ] && [ "$elapsed" -le "$min_elapsed" ]; then
burn=" โ‹ฎ ๐Ÿ”ฅ ${min_label} โณ"
fi
echo "${emoji} ${pct}% (${left}) ${dot}${burn}"
}
# โ”€โ”€ Per-model weekly quotas (OAuth usage API) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
file_mtime() {
local m
# GNU/BusyBox FIRST, and the order is load-bearing: on those `stat -f` means
# FILESYSTEM stat, which prints "Block size: 4096 ..." to stdout and exits 0
# โ€” so a BSD-first `||` chain never falls through and that text becomes the
# timestamp, leaking into the rendered bar. macOS has no `stat -c`, so it
# fails cleanly and takes the BSD branch.
m=$(stat -c %Y "$1" 2>/dev/null) || m=$(stat -f %m "$1" 2>/dev/null)
case "${m:-}" in ''|*[!0-9]*) echo 0 ;; *) echo "$m" ;; esac
}
cache_age() {
[ -f "$USAGE_CACHE" ] || { echo 999999; return; }
local age=$((NOW - $(file_mtime "$USAGE_CACHE")))
[ "$age" -lt 0 ] && age=0
echo "$age"
}
# macOS: OAuth blob lives in the Keychain; Linux/WSL: plaintext credentials file.
get_oauth_token() {
local token=""
if command -v security >/dev/null 2>&1; then
token=$(security find-generic-password -s "$KEYCHAIN_SERVICE" -w 2>/dev/null \
| jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
fi
if [ -z "$token" ] && [ -f "$CREDENTIALS_FILE" ]; then
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDENTIALS_FILE" 2>/dev/null)
fi
printf '%s' "$token"
}
refresh_usage() {
local token resp tmp
token=$(get_oauth_token)
[ -z "$token" ] && return 1
resp=$(curl -s --max-time 3 "https://api.anthropic.com/api/oauth/usage" \
-H "Authorization: Bearer $token" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "Content-Type: application/json" 2>/dev/null)
printf '%s' "$resp" | jq -e '.five_hour.utilization' >/dev/null 2>&1 || return 1
tmp=$(mktemp "${USAGE_CACHE}.XXXXXX") || return 1
if printf '%s' "$resp" > "$tmp"; then
mv -f "$tmp" "$USAGE_CACHE"
else
rm -f "$tmp"
return 1
fi
}
# Refresh in the background so a render never blocks on the network. The mkdir
# lock is atomic on bash 3.2/macOS (no flock needed); a lock dir older than 60s
# is a crashed refresh and gets reclaimed.
maybe_refresh_usage() {
[ "$(cache_age)" -le "$REFRESH_INTERVAL" ] && return
# Reclaim a crashed refresh's lock (or any foreign file squatting on the
# path) once it is older than 60s.
if [ -e "$LOCK_DIR" ] && [ $((NOW - $(file_mtime "$LOCK_DIR"))) -gt 60 ]; then
rm -rf "$LOCK_DIR" 2>/dev/null
fi
mkdir "$LOCK_DIR" 2>/dev/null || return
( refresh_usage; rmdir "$LOCK_DIR" 2>/dev/null ) >/dev/null 2>&1 &
}
# "Fable: 31% ๐ŸŸข / Snt: 12% ๐ŸŸข" โ€” per-model weekly quotas. Prefers the modern
# .limits[] weekly_scoped entries; falls back to the legacy seven_day_<model>
# keys for older API responses. @tsv keeps the jq free of escape sequences.
weekly_models_part() {
[ -f "$USAGE_CACHE" ] || return
local out="" key pct label dot
while IFS=$'\t' read -r key pct; do
[ -z "$key" ] && continue
pct=$(printf '%.0f' "$pct" 2>/dev/null) || continue
case "$key" in
[Ff]able*) label="Fable" ;;
[Ss]onnet*) label="Snt" ;;
[Oo]pus*) label="Opus" ;;
[Hh]aiku*) label="Hku" ;;
[Mm]ythos*) label="Myth" ;;
*) label=$(sanitize "$key") ;;
esac
dot=$(usage_dot "$pct")
[ -n "$out" ] && out="$out / "
out="${out}${label}: ${pct}% ${dot}"
done <<EOF
$(jq -r '
([.limits // [] | .[]
| select(.kind == "weekly_scoped" and .scope != null and .scope.model != null and .percent != null)
| [(.scope.model.display_name // .scope.model.id // "model"), .percent]]
) as $scoped
| (if ($scoped | length) > 0 then $scoped
else [to_entries[]
| select(.key | startswith("seven_day_"))
| select(.value != null and (.value | type == "object") and .value.utilization != null)
| [(.key | sub("seven_day_"; "")), .value.utilization]]
end)
| .[] | @tsv' "$USAGE_CACHE" 2>/dev/null)
EOF
printf '%s' "$out"
}
# โ”€โ”€ Parse stdin in a single jq pass โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Fields joined on the unit separator so a "|" inside a branch or model name
# cannot shift columns.
INPUT=$(cat)
NOW=$(date +%s)
[ "$USE_USAGE_API" = "1" ] && maybe_refresh_usage
FIELDS=$(printf '%s' "$INPUT" | jq -r '[
(.workspace.current_dir // .cwd // ""),
(.model | if type == "object" then (.display_name // .id // "") else "" end),
(.effort.level // ""),
(if .context_window.current_usage == null then "0" else "1" end),
(.context_window.current_usage.input_tokens // 0 | tostring),
(.context_window.current_usage.cache_creation_input_tokens // 0 | tostring),
(.context_window.current_usage.cache_read_input_tokens // 0 | tostring),
(.context_window.context_window_size // 0 | tostring),
(.rate_limits.five_hour.used_percentage // "" | tostring),
(.rate_limits.five_hour.resets_at // "" | tostring),
(.rate_limits.seven_day.used_percentage // "" | tostring),
(.rate_limits.seven_day.resets_at // "" | tostring),
(.pr.number // "" | tostring),
(.pr.review_state // "")
] | join("\u001f")' 2>/dev/null)
IFS=$'\x1f' read -r CWD MODEL EFFORT HAVE_USAGE ITOK CCTOK CRTOK CTXSZ \
R5H R5H_RESET R7D R7D_RESET PR_NUM PR_STATE <<< "$FIELDS"
# stdin may give resets_at as ISO 8601, but merge_quota and quota_segment both
# do epoch ARITHMETIC on it โ€” an unconverted ISO value errors ("value too great
# for base") and blanks both quota segments. Guarded on non-numeric, so an
# already-epoch value passes through untouched. (Masked on macOS when the usage
# cache wins the merge and supplies the epoch; bites the USE_USAGE_API=0 path.)
case "${R5H_RESET:-}" in "") ;; *[!0-9]*) R5H_RESET=$(iso_to_epoch "$R5H_RESET") ;; esac
case "${R7D_RESET:-}" in "") ;; *[!0-9]*) R7D_RESET=$(iso_to_epoch "$R7D_RESET") ;; esac
# โ”€โ”€ Account-fresh quota merge + burn history โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# The usage cache is shared by every session and refreshed by whichever
# renders first, so it can be fresher than this session's stdin when another
# session is doing the burning.
if [ "$USE_USAGE_API" = "1" ] && [ -f "$USAGE_CACHE" ]; then
IFS=$'\t' read -r C5P C5R C7P C7R <<EOF
$(jq -r '[(.five_hour.utilization // ""), (.five_hour.resets_at // ""),
(.seven_day.utilization // ""), (.seven_day.resets_at // "")] | @tsv' "$USAGE_CACHE" 2>/dev/null)
EOF
[ -n "$C5R" ] && C5R=$(iso_to_epoch "$C5R")
[ -n "$C7R" ] && C7R=$(iso_to_epoch "$C7R")
merged=$(merge_quota "$R5H" "$R5H_RESET" "$C5P" "$C5R")
R5H=${merged%%|*}; R5H_RESET=${merged##*|}
merged=$(merge_quota "$R7D" "$R7D_RESET" "$C7P" "$C7R")
R7D=${merged%%|*}; R7D_RESET=${merged##*|}
fi
# One sample per ~30s across all sessions: "ts 5h% 5h_reset 7d% 7d_reset".
# Sliding-window burn rates read this; entries older than 8 days are pruned.
if [ -n "$R5H" ] || [ -n "$R7D" ]; then
last_ts=$(tail -1 "$HISTORY_FILE" 2>/dev/null | awk '{print $1}')
case "$last_ts" in
''|*[!0-9]*) last_ts=0 ;;
esac
if [ $((NOW - last_ts)) -ge 30 ]; then
h5p="-"; h5r="-"; h7p="-"; h7r="-"
[ -n "$R5H" ] && h5p=$(printf '%.0f' "$R5H" 2>/dev/null)
[ -n "$R5H_RESET" ] && h5r="$R5H_RESET"
[ -n "$R7D" ] && h7p=$(printf '%.0f' "$R7D" 2>/dev/null)
[ -n "$R7D_RESET" ] && h7r="$R7D_RESET"
echo "$NOW ${h5p:--} ${h5r} ${h7p:--} ${h7r}" >> "$HISTORY_FILE" 2>/dev/null
first_ts=$(head -1 "$HISTORY_FILE" 2>/dev/null | awk '{print $1}')
case "$first_ts" in
''|*[!0-9]*) first_ts=$NOW ;;
esac
if [ $((NOW - first_ts)) -gt 691200 ]; then
tmp=$(mktemp "${HISTORY_FILE}.XXXXXX") \
&& awk -v cut=$((NOW - 691200)) '$1+0 >= cut' "$HISTORY_FILE" > "$tmp" 2>/dev/null \
&& mv -f "$tmp" "$HISTORY_FILE"
fi
fi
fi
# โ”€โ”€ Row 1: path + git โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
[ -z "$CWD" ] && CWD="$PWD"
if [ -n "$HOME" ] && [ "${CWD#$HOME}" != "$CWD" ]; then
display_path="~${CWD#$HOME}"
else
display_path="$CWD"
fi
# Machine indicator: ๐Ÿ”’ sandbox / ๐Ÿ‹ container / ๐Ÿ–ฅ๏ธ normal, with the short
# hostname (no FQDN). The path always keeps ๐Ÿ“.
if [ -n "$IS_SANDBOX" ]; then
host_emoji="๐Ÿ”’"
elif [ -f /.dockerenv ] || [ -f /run/.containerenv ] \
|| grep -qE '(docker|kubepods|containerd|lxc)' /proc/1/cgroup 2>/dev/null; then
host_emoji="๐Ÿ‹"
else
host_emoji="๐Ÿ–ฅ๏ธ"
fi
host_short=$(hostname -s 2>/dev/null || hostname 2>/dev/null)
host_short="${host_short%%.*}"
host_short=$(sanitize "$host_short")
line1="${host_emoji} ${host_short} | ๐Ÿ“ ${display_path}"
if [ -d "$CWD" ] && git -C "$CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
gs=$(git -C "$CWD" --no-optional-locks status --porcelain=v2 --branch 2>/dev/null)
parsed=$(echo "$gs" | awk '
BEGIN { branch="-"; upstream="-"; oid="-"; ahead="+0"; behind="-0"
staged=0; modified=0; untracked=0; unmerged=0 }
/^# branch\.oid/ { oid=$3 }
/^# branch\.head/ { branch=$3 }
/^# branch\.upstream/ { upstream=$3 }
/^# branch\.ab/ { ahead=$3; behind=$4 }
/^[12] / { x=substr($2,1,1); y=substr($2,2,1)
if (x != ".") staged++
if (y != ".") modified++ }
/^\? / { untracked++ }
/^u / { unmerged++ }
END { printf "%s\t%s\t%s\t%s\t%s\t%d\t%d\t%d\t%d",
branch, upstream, oid, ahead, behind,
staged, modified, untracked, unmerged }')
IFS=$'\t' read -r branch upstream oid ahead behind staged modified untracked unmerged <<< "$parsed"
branch=$(sanitize "$branch")
upstream=$(sanitize "$upstream")
if [ "$branch" = "(detached)" ]; then
git_part="๐ŸŒฟ detached@${oid:0:7}"
else
git_part="๐ŸŒฟ ${branch}"
fi
if [ "$upstream" != "-" ]; then
git_part="${git_part} โžก๏ธ ${upstream}"
else
git_part="${git_part} (no upstream)"
fi
ahead_num=${ahead#+}
behind_num=${behind#-}
[ "${ahead_num:-0}" != "0" ] && git_part="${git_part} โฌ†๏ธ ahead: ${ahead_num}"
[ "${behind_num:-0}" != "0" ] && git_part="${git_part} โฌ‡๏ธ behind: ${behind_num}"
changes=""
[ "$staged" -gt 0 ] && changes="${changes} ๐Ÿ“ฅ staged: ${staged}"
[ "$modified" -gt 0 ] && changes="${changes} ๐Ÿ“ mod: ${modified}"
[ "$unmerged" -gt 0 ] && changes="${changes} ๐Ÿ’ฅ conflict: ${unmerged}"
[ "$untracked" -gt 0 ] && changes="${changes} ๐Ÿ†• new: ${untracked}"
if [ -n "$changes" ]; then
git_part="${git_part} |${changes}"
else
git_part="${git_part} โœ… clean"
fi
line1="${line1} | ${git_part}"
fi
if [ "$SHOW_PR" = "1" ] && [ -n "$PR_NUM" ]; then
pr_icon=""
case "$PR_STATE" in
approved) pr_icon=" โœ“" ;;
changes_requested) pr_icon=" โœ—" ;;
pending) pr_icon=" โ—‹" ;;
draft) pr_icon=" โ—‡" ;;
esac
line1="${line1} | ๐Ÿ”€ PR#$(sanitize "$PR_NUM")${pr_icon}"
fi
# โ”€โ”€ Row 2: model + effort, context / memory / autocompact, battery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if [ -z "$EFFORT" ] && [ -f "$SETTINGS_FILE" ]; then
EFFORT=$(jq -r '.effortLevel // empty' "$SETTINGS_FILE" 2>/dev/null)
fi
MODEL=$(sanitize "$MODEL")
[ -n "$EFFORT" ] && MODEL="${MODEL} โ‹ฎ ๐Ÿ’ช ${EFFORT}"
if [ -n "$MODEL" ]; then out="๐Ÿค– ${MODEL}"; else out=""; fi
if [ "$HAVE_USAGE" = "1" ] && [ "${CTXSZ:-0}" -gt 0 ] 2>/dev/null; then
cur=$((ITOK + CCTOK + CRTOK))
ctxpct=$((cur * 100 / CTXSZ))
cthou=$((cur / 1000))
ctxm=$((CTXSZ / 1000000))
if [ "$ctxm" -gt 0 ]; then ctxlbl="${ctxm}M"; else ctxlbl="$((CTXSZ / 1000))k"; fi
mem_k=$((MEM_LIMIT / 1000))
mem_pct=$((cur * 100 / MEM_LIMIT))
mem_dot=$(usage_dot "$mem_pct")
mem_seg="๐Ÿง  ${cthou}k/${ctxlbl} (${ctxpct}%) โ‹ฎ ๐Ÿง˜ ${cthou}k/${mem_k}k (${mem_pct}%) ${mem_dot}"
if [ "$SHOW_WEEKLY_MODELS" = "1" ]; then
models=$(weekly_models_part)
[ -n "$models" ] && mem_seg="${mem_seg} โ‹ฎ ${models}"
fi
out="$out | ${mem_seg}"
cpctdf=${CLAUDE_AUTOCOMPACT_PCT_OVERRIDE:-95}
compact=${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-$((CTXSZ * cpctdf / 100))}
if [ "$compact" -gt 0 ]; then
cpct=$((cur * 100 / compact))
ctgt=$((compact / 1000))
dot=$(usage_dot "$cpct")
curk=$((cur / 1000))
out="$out | โ™ป๏ธ ${curk}k/${ctgt}k (${cpct}%) ${dot}"
fi
fi
line2="${out# | }"
batt_seg=""
if command -v pmset >/dev/null 2>&1; then
batt=$(pmset -g batt 2>/dev/null)
pct=$(echo "$batt" | grep -oE '[0-9]+%' | head -1)
if [ -n "$pct" ]; then
if echo "$batt" | grep -q "AC Power"; then
batt_seg="๐Ÿ”Œ plugged in ๐ŸŸข"
else
num=${pct%\%}
rem_str=""
time_rem=$(echo "$batt" | grep -oE '[0-9]+:[0-9]+ remaining' | head -1)
if [ -n "$time_rem" ]; then
hh=${time_rem%%:*}; mm=${time_rem#*:}; mm=${mm%% *}
total_secs=$((10#$hh * 3600 + 10#$mm * 60))
[ "$total_secs" -gt 0 ] && rem_str=" ($(fmt_dur $total_secs))"
fi
if [ "$num" -le 20 ]; then batt_seg="๐Ÿชซ ${pct}${rem_str} ๐Ÿšจ"
elif [ "$num" -lt 50 ]; then batt_seg="๐Ÿ”‹ ${pct}${rem_str} โš ๏ธ"
else batt_seg="๐Ÿ”‹ ${pct}${rem_str} ๐ŸŸข"
fi
fi
fi
elif [ -d /sys/class/power_supply ]; then
bat_dir=""
for d in /sys/class/power_supply/BAT*; do
[ -d "$d" ] && bat_dir="$d" && break
done
if [ -n "$bat_dir" ] && [ -f "$bat_dir/capacity" ]; then
num=$(cat "$bat_dir/capacity" 2>/dev/null)
pct="${num}%"
on_ac=0
for a in /sys/class/power_supply/A*; do
[ -f "$a/online" ] && [ "$(cat "$a/online" 2>/dev/null)" = "1" ] && on_ac=1 && break
done
if [ "$on_ac" = "1" ]; then
batt_seg="๐Ÿ”Œ plugged in ๐ŸŸข"
else
rem_str=""
time_rem=$(acpi -b 2>/dev/null | grep -oE '[0-9]+:[0-9]+:[0-9]+' | head -1)
if [ -n "$time_rem" ]; then
hh=${time_rem%%:*}; rest=${time_rem#*:}; mm=${rest%%:*}
total_secs=$((10#$hh * 3600 + 10#$mm * 60))
[ "$total_secs" -gt 0 ] && rem_str=" ($(fmt_dur $total_secs))"
fi
if [ "$num" -le 20 ]; then batt_seg="๐Ÿชซ ${pct}${rem_str} ๐Ÿšจ"
elif [ "$num" -lt 50 ]; then batt_seg="๐Ÿ”‹ ${pct}${rem_str} โš ๏ธ"
else batt_seg="๐Ÿ”‹ ${pct}${rem_str} ๐ŸŸข"
fi
fi
fi
fi
[ -n "$batt_seg" ] && line2="${line2} | ${batt_seg}"
# โ”€โ”€ Row 3: clock, 5h reset flag, 5h + 7d quotas with burn rates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
out=""
if [ -n "$R5H" ]; then
seg=$(quota_segment "โฒ๏ธ" "$R5H" "$R5H_RESET" 18000 1800 3600 "%/h" 20 25 "<30m data" 3600 600 2 3)
out="$out | $seg"
else
out="$out | โฒ๏ธ N/A"
fi
if [ -n "$R7D" ]; then
seg=$(quota_segment "๐Ÿ“…" "$R7D" "$R7D_RESET" 604800 86400 86400 "%/d avg" 14 17 "<1d data" 86400 7200 4 5)
out="$out | $seg"
else
out="$out | ๐Ÿ“… N/A"
fi
flag_seg=""
if [ -n "$R5H_RESET" ]; then
r5h_clock=$(date -r "$R5H_RESET" +"%I:%M %p" 2>/dev/null || date -d "@$R5H_RESET" +"%I:%M %p" 2>/dev/null)
r5h_clock="${r5h_clock#0}"
[ -n "$r5h_clock" ] && flag_seg="๐Ÿ ${r5h_clock} โฒ๏ธ"
fi
if [ "$SHOW_WEEK_DATE" = "1" ] && [ -n "$R7D_RESET" ]; then
r7d_date=$(date -r "$R7D_RESET" +"%a %m/%d" 2>/dev/null || date -d "@$R7D_RESET" +"%a %m/%d" 2>/dev/null)
r7d_date=$(echo "$r7d_date" | sed -e 's| 0| |' -e 's|/0|/|')
if [ -n "$r7d_date" ]; then
if [ -n "$flag_seg" ]; then
flag_seg="${flag_seg} โ‹ฎ ๐Ÿณ๏ธ ${r7d_date} ๐Ÿ“…"
else
flag_seg="๐Ÿณ๏ธ ${r7d_date} ๐Ÿ“…"
fi
fi
fi
[ -n "$flag_seg" ] && flag_seg="${flag_seg} | "
clock=$(date +"%I:%M %p")
clock="${clock#0}"
sep="|"
[ -n "$flag_seg" ] && sep="โ‹ฎ"
line3="โฐ ${clock} ${sep} ${flag_seg}${out# | }"
# Style the separators: | gets bold bright-blue on black (strong major break);
# โ‹ฎ gets a pale near-white blue (quiet, sits close to the text color) โ€” or,
# with SUBSEP_FG=none, is left in the terminal's default foreground so it
# matches the font exactly. All other text is untouched; each glyph
# self-resets so no color bleeds.
# Row 4: static keybinding hints.
[ "$SHOW_HINTS" = "1" ] && line4="$HINTS_TEXT"
if [ "$SHOW_PIPE_STYLE" = "1" ]; then
esc=$(printf '\033')
sp="${esc}[1;38;5;${PIPE_FG};48;5;${PIPE_BG}m|${esc}[0m"
line1="${line1//|/$sp}"
line2="${line2//|/$sp}"
line3="${line3//|/$sp}"
line4="${line4//|/$sp}"
case "$SUBSEP_FG" in
''|none|default) : ;; # leave โ‹ฎ in the default foreground (exact font match)
*)
ssp="${esc}[38;5;${SUBSEP_FG}mโ‹ฎ${esc}[0m"
line1="${line1//โ‹ฎ/$ssp}"
line2="${line2//โ‹ฎ/$ssp}"
line3="${line3//โ‹ฎ/$ssp}"
line4="${line4//โ‹ฎ/$ssp}"
;;
esac
fi
printf '%s\n%s\n%s' "$line1" "$line2" "$line3"
[ "$SHOW_HINTS" = "1" ] && printf '\n%s' "$line4"

Claude Code Statusbar โ€” Sync Protocol

How to reconcile the status bar between this gist and a local ~/.claude/settings.json. Hand this file (or its gist URL) to Claude Code and say "run the sync protocol".

Config

  • GIST: 26ee6f961ba6bd7a436d745adb99657b (pass the bare id to gh gist, not owner/id)
  • LOCAL: ~/.claude/settings.json โ€” these keys are in scope; every other key must be preserved untouched: statusLine, env, cleanupPeriodDays, showClearContextOnPlanAccept, effortLevel, tui, voice, attribution. (model was deliberately dropped from scope โ€” it churns on every /model.) The canonical scope() filter (Step 1) is the single source of truth for this list โ€” add/remove a key there and every step follows.
  • SCRIPT: ~/.claude/statusline.sh โ€” the status bar script that statusLine.command points at; synced 1:1 with the gist's statusline.sh
  • Gist files:
    • settings.json โ€” the in-scope subset of the local settings (see scope())
    • statusline.sh โ€” the status bar script (bash 3.2 / macOS stock bash)
    • Claude Code Status Bar.md โ€” legend + a single rendered example (format below)
    • sync-protocol.md โ€” this file

Step 1 โ€” Fetch

gh gist view --raw and gh api ... .content mis-decode backslash-u escapes stored literally in gist files (details in Step 6), so fetch gist content through each file's raw_url. scope() picks the in-scope settings keys โ€” the one place the scope is defined:

gist_raw() { curl -s "$(gh api gists/<GIST> --jq ".files.\"$1\".raw_url")"; }
scope()    { jq -S '{statusLine, env, cleanupPeriodDays, showClearContextOnPlanAccept, effortLevel, tui, voice, attribution}' "$@"; }
  • Gist JSON: gist_raw settings.json
  • Gist script: gist_raw statusline.sh
  • Gist example: gist_raw "Claude Code Status Bar.md"
  • Local: scope <LOCAL> and <SCRIPT>
  • List files: gh gist view <GIST> --files (confirm filenames before editing โ€” the config file is settings.json, the script is statusline.sh, the example file is Claude Code Status Bar.md, singular)

Step 2 โ€” Compare

Diff the in-scope keys and the script:

diff <(gist_raw settings.json | scope) <(scope <LOCAL>)
diff <(gist_raw statusline.sh) <SCRIPT>

If both are identical, stop โ€” report "in sync", nothing to write.

If they differ, parse the script (or statusLine.command, wherever the logic lives) semantically and present an inventory table of every discrete segment (path/sandbox/container icon, git branch + upstream + ahead/behind + dirty counts, PR indicator, model, context, autocompact, 5h limit, 7d limit, per-model weekly quotas, burn rates, ETAs, thresholds, fallback labels) plus any in-scope settings differences (env, effortLevel, tui, โ€ฆ): present in gist? in local? identical? if differing, what differs?

Pause. The user picks a direction per divergence (usually: local is newer, push local โ†’ gist). No writes yet.

Note: the gist is PUBLIC. The in-scope keys (env, effortLevel, tui, โ€ฆ) are personal preferences, not secrets โ€” but never let a credential-bearing key into scope.

Step 3 โ€” Approve merged JSON + script

Show the final merged in-scope settings block (scope) and, when the script diverged, a summary of the merged script's changes. Wait for explicit approval.

Step 4 โ€” Regenerate Claude Code Status Bar.md

Keep the existing format exactly โ€” do not redesign it:

  • # Emoji Legend โ€” one symbol per line, EMOJI Label (Title Case), blank line between entries. Cover every symbol the merged script can emit.
  • # Example โ€” one fenced block, three lines, each indented by two spaces: line 1 = host/project/git/PR, line 2 = model/context/per-model/battery, line 3 = clock/reset flags/limits/burn.

Derive the legend from the merged script (symbols drift). Capture the example lines by actually running the merged script against a mock payload + a throwaway git fixture for accuracy, then present it with an illustrative path (~/Repositories/project) to match house style. Reproduce with:

echo '{"workspace":{"current_dir":"<dir>"},"model":{...},"context_window":{...},"rate_limits":{...}}' | bash <SCRIPT>

resets_at values are absolute epoch seconds โ€” offset from date +%s. The script also reads optional cache/history files; point USAGE_CACHE and HISTORY_FILE at fixture copies (and set USE_USAGE_API=0) to make the render deterministic. Strip ANSI (sed $'s/\033\\[[0-9;]*m//g') for the plain-text example โ€” the separator coloring can't be shown in a fenced block.

Show the regenerated Claude Code Status Bar.md and wait for approval.

Step 5 โ€” Write (only after BOTH approvals)

  • Backup local: cp <LOCAL> <LOCAL>.bak.$(date +%Y%m%d-%H%M%S) and cp <SCRIPT> <SCRIPT>.bak.$(date +%Y%m%d-%H%M%S) (skip whichever is not being written)
  • Local JSON (import gist โ†’ local): overlay only the in-scope keys, preserving every other local key: gist_raw settings.json > <g> && jq --slurpfile n <g> '. as $l | ["statusLine","env","cleanupPeriodDays","showClearContextOnPlanAccept","effortLevel","tui","voice","attribution"] as $ks | reduce $ks[] as $k ($l; .[$k] = $n[0][$k])' <LOCAL> > <tmp2> && mv <tmp2> <LOCAL>
  • Local script: copy the merged script over <SCRIPT> and chmod +x it
  • Gist JSON (export local โ†’ gist): scope <LOCAL> > <tmpfile> (basename settings.json), then gh gist edit <GIST> -f settings.json <tmpfile>
  • Gist script: gh gist edit <GIST> -f statusline.sh <tmpfile>
  • Gist example: gh gist edit <GIST> -f "Claude Code Status Bar.md" <tmpfile>
  • Note: gh gist edit -f only updates an existing file. To add a new file use --add <path>; to remove use --remove <name>; a rename is add + remove. --add/-f use the temp file's basename as the gist filename.

Step 6 โ€” Verify

Re-fetch the gist files with gist_raw; diff against what was written and against scope <LOCAL> / <SCRIPT>. Confirm all non-scope keys still present in <LOCAL>. Report the backup path and a final manifest.

Note: gh gist view --raw can mis-decode backslash-u escape sequences that appear literally in file content (e.g. a join("\u001f") inside a jq program), producing false diffs. Before treating a byte-level difference as real, confirm with gist_raw <name> (Step 1), which reads the file's true bytes via its raw_url โ€” never trust a diff sourced from --raw output.

Safety

  • No destructive git/file operations.
  • Preserve every key outside the scope() set in <LOCAL>.
  • The gist is PUBLIC โ€” never let a credential-bearing key into scope().
  • No partial writes: nothing is written until both the merged JSON/script and the regenerated Claude Code Status Bar.md are approved.
  • The gist is GitHub-versioned, so gist writes are recoverable; local is not โ€” hence the timestamped local backups.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment