Skip to content

Instantly share code, notes, and snippets.

@ungoldman
Created July 16, 2026 18:13
Show Gist options
  • Select an option

  • Save ungoldman/da9b4dc679f78344b4087de52b2a06f2 to your computer and use it in GitHub Desktop.

Select an option

Save ungoldman/da9b4dc679f78344b4087de52b2a06f2 to your computer and use it in GitHub Desktop.
statusline.sh
#!/usr/bin/env bash
# Claude Code status line. Two lines under your prompt.
#
# Line 1: 📁 dir 🌿 branch+state 🤖 model (effort) 📟 version 🔑 session
# Line 2: ⏳ 5h: X% (↻reset) 📅 7d: X% (↻reset) 🧠 Ctx: X% (Nk) 🎟️ session tokens
#
# 📁 dir working directory, home shortened to ~
# 🌿 branch+state git branch; * = dirty (incl. untracked), ↑N/↓N = ahead/behind upstream
# 🤖 model (effort) active model, with reasoning-effort level when set
# 📟 version Claude Code version
# 🔑 session first 8 chars of the session id
# ⏳ 5h / 📅 7d rolling 5-hour and 7-day rate-limit usage %, with ↻ local reset time
# 🧠 Ctx context-window fill %, plus (Nk) tokens used so far
# 🎟️ session tokens tokens processed this session (input + output + cache-creation)
# └ new work only: Σ(input + output + cache_creation) per turn; omits cache_read (re-read context)
#
# Usage % reads your own local OAuth token via an undocumented endpoint (goes
# blank if unavailable). Needs jq + curl. Works on macOS (BSD) and Linux (GNU).
#
# shellcheck disable=SC2154 # vars are set by the eval'd jq @sh blocks below
# shellcheck disable=SC2312 # every external call is best-effort; a statusline
# # must never fail, so masked exit codes are intended
# Toggles: set to 0 to hide, or override per run (e.g. SHOW_USAGE=0).
SHOW_USAGE="${SHOW_USAGE:-1}" # ⏳ 📅 rate-limit % (network)
SHOW_SESSION_ID="${SHOW_SESSION_ID:-1}" # 🔑 session id
SHOW_SESSION_TOKENS="${SHOW_SESSION_TOKENS:-1}" # 🎟️ session token throughput
input=$(cat)
# --- Cross-platform helpers (macOS BSD vs Linux GNU coreutils) ---
if stat --version >/dev/null 2>&1; then STAT_FLAVOR=gnu; else STAT_FLAVOR=bsd; fi
if date --version >/dev/null 2>&1; then DATE_FLAVOR=gnu; else DATE_FLAVOR=bsd; fi
file_mtime() { # path -> mtime epoch (0 if missing)
if [[ "${STAT_FLAVOR}" = gnu ]]; then stat -c '%Y' "${1}" 2>/dev/null || echo 0
else stat -f '%m' "${1}" 2>/dev/null || echo 0; fi
}
iso_to_epoch() { # ISO8601 (no fraction/zone) -> epoch
if [[ "${DATE_FLAVOR}" = gnu ]]; then date -u -d "${1}" +%s 2>/dev/null
else date -u -j -f "%Y-%m-%dT%H:%M:%S" "${1}" +%s 2>/dev/null; fi
}
epoch_fmt() { # epoch, strftime fmt -> local time
if [[ "${DATE_FLAVOR}" = gnu ]]; then date -d "@${1}" "+${2}" 2>/dev/null
else date -r "${1}" "+${2}" 2>/dev/null; fi
}
# --- Extract fields from Claude Code JSON ---
eval "$(jq -r '
@sh "model=\(.model.display_name // "Unknown" | sub(" \\(.*\\)$"; ""))",
@sh "effort=\(.effort.level // "")",
@sh "version=\(.version // "")",
@sh "used_pct=\(.context_window.used_percentage // 0)",
@sh "used_tokens=\(((.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0)))",
@sh "cwd=\(.workspace.current_dir // .cwd // "")",
@sh "session_id=\(.sessionId // .session_id // "")",
@sh "transcript_path=\(.transcript_path // "")"
' <<< "${input}" 2>/dev/null)"
# --- Derived values ---
dir="${cwd:-$(pwd)}"
short_dir="${dir/#${HOME}/~}"
# shellcheck disable=SC2088 # literal ~/ is for display, not expansion
[[ "${short_dir}" = "~" ]] && short_dir="~/"
branch=""
if gb=$(git -C "${dir}" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null); then
branch="${gb}"
elif gb=$(git -C "${dir}" --no-optional-locks rev-parse --short HEAD 2>/dev/null); then
branch="detached:${gb}"
fi
# Working-tree state: "*" if dirty (incl. untracked), "↑N"/"↓N" vs upstream.
git_state=""
if [[ -n "${branch}" ]]; then
[[ -n "$(git -C "${dir}" --no-optional-locks status --porcelain 2>/dev/null)" ]] && git_state+="*"
if ab=$(git -C "${dir}" --no-optional-locks rev-list --count --left-right '@{upstream}...HEAD' 2>/dev/null); then
behind=${ab%% *}; ahead=${ab##* }
[[ "${ahead:-0}" -gt 0 ]] && git_state+=" ↑${ahead}"
[[ "${behind:-0}" -gt 0 ]] && git_state+=" ↓${behind}"
fi
fi
# --- Usage from Anthropic API (your own token; cached 2 min) ---
usage_5h="" usage_7d="" resets_5h="" resets_7d=""
if [[ "${SHOW_USAGE}" = 1 ]]; then
CACHE="${HOME}/.cache/cc-usage.json"
TTL=120
now=$(date +%s)
mkdir -p "${HOME}/.cache"
fetch_usage=1
if [[ -f "${CACHE}" ]]; then
age=$(( now - $(file_mtime "${CACHE}") ))
[[ "${age}" -lt "${TTL}" ]] && fetch_usage=0
fi
if [[ "${fetch_usage}" -eq 1 ]]; then
token=$(jq -r '.claudeAiOauth.accessToken // empty' "${HOME}/.claude/.credentials.json" 2>/dev/null)
token=${token:-$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)}
if [[ -n "${token}" ]]; then
resp=$(curl -s --max-time 5 \
"https://api.anthropic.com/api/oauth/usage" \
-H "Authorization: Bearer ${token}" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "User-Agent: claude-code/${version}" 2>/dev/null)
if [[ -n "${resp}" ]] && jq -e '.five_hour' <<< "${resp}" >/dev/null 2>&1; then
echo "${resp}" > "${CACHE}"
else
touch "${CACHE}" # cache failures too, to avoid hammering the API
fi
fi
fi
if [[ -f "${CACHE}" ]]; then
eval "$(jq -r '
@sh "usage_5h=\(.five_hour.utilization // "")",
@sh "usage_7d=\(.seven_day.utilization // "")",
@sh "resets_5h=\(.five_hour.resets_at // "")",
@sh "resets_7d=\(.seven_day.resets_at // "")"
' "${CACHE}" 2>/dev/null)"
fi
fi
# Local clock time an ISO (UTC) reset lands at: "3pm" / "3:44pm", or "wed 3pm".
format_reset() {
local ts="${1%%.*}" reset_sec timefmt
reset_sec=$(iso_to_epoch "${ts}") || return
[[ -n "${reset_sec}" ]] || return
if [[ "$(epoch_fmt "${reset_sec}" %M)" = "00" ]]; then timefmt="%-I%p"; else timefmt="%-I:%M%p"; fi
[[ "${today_j}" != "$(epoch_fmt "${reset_sec}" %j)" ]] && timefmt="%a ${timefmt}"
epoch_fmt "${reset_sec}" "${timefmt}" | tr '[:upper:]' '[:lower:]'
}
today_j=$(date +%j) # "now" day-of-year, computed once for format_reset
# --- Session token throughput (in + out + cache_creation; excludes cache_read) ---
# The scan is O(transcript size) and the line re-renders many times between
# turns, so cache the sum and only rescan when the transcript's mtime moves.
session_tokens=0
if [[ "${SHOW_SESSION_TOKENS}" = 1 ]]; then
transcript="${transcript_path}"
if [[ -z "${transcript}" ]] && [[ -n "${session_id}" ]] && [[ -n "${dir}" ]]; then
enc=$(printf '%s' "${dir}" | sed 's/[^a-zA-Z0-9]/-/g')
transcript="${HOME}/.claude/projects/${enc}/${session_id}.jsonl"
fi
if [[ -n "${transcript}" ]] && [[ -f "${transcript}" ]]; then
TOK_CACHE="${HOME}/.cache/cc-tokens.json"
t_mtime=$(file_mtime "${transcript}")
[[ -f "${TOK_CACHE}" ]] && eval "$(jq -r '
@sh "c_file=\(.transcript // "")", @sh "c_mtime=\(.mtime // 0)", @sh "c_tokens=\(.tokens // 0)"
' "${TOK_CACHE}" 2>/dev/null)"
if [[ "${c_file:-}" = "${transcript}" ]] && [[ "${c_mtime:-}" = "${t_mtime}" ]]; then
session_tokens=${c_tokens}
else
session_tokens=$(jq -n 'reduce inputs as $r (0;
if $r.type == "assistant" and ($r.message.usage)
then . + (($r.message.usage.input_tokens // 0)
+ ($r.message.usage.output_tokens // 0)
+ ($r.message.usage.cache_creation_input_tokens // 0))
else . end)' "${transcript}" 2>/dev/null)
session_tokens=${session_tokens:-0}
mkdir -p "${HOME}/.cache"
printf '{"transcript":"%s","mtime":%s,"tokens":%s}\n' \
"${transcript}" "${t_mtime:-0}" "${session_tokens}" > "${TOK_CACHE}" 2>/dev/null
fi
fi
fi
# Compact token count: 1234 -> 1.2k, 1234567 -> 1.2M
fmt_tokens() {
local n=${1}
if (( n >= 1000000 )); then printf '%d.%dM' $(( n / 1000000 )) $(( (n % 1000000) / 100000 ))
elif (( n >= 1000 )); then printf '%d.%dk' $(( n / 1000 )) $(( (n % 1000) / 100 ))
else printf '%d' "${n}"
fi
}
# pct + low/mid/high colors -> the color for that tier (mid at 50%, high at 80%)
pct_color() {
local pct=${1%.*}; pct=${pct:-0}
if (( pct >= 80 )); then printf '%s' "${4}"
elif (( pct >= 50 )); then printf '%s' "${3}"
else printf '%s' "${2}"
fi
}
# icon label pct resets -> a formatted usage segment, or "" when pct is empty
usage_seg() {
[[ -n "${3}" ]] || return
local clr reset
clr=$(pct_color "${3}" '\e[38;5;194m' '\e[38;5;228m' '\e[38;5;210m')
[[ -n "${4}" ]] && reset=$(format_reset "${4}")
printf '%s%s %s: %.0f%%%s%s' "${clr}" "${1}" "${2}" "${3}" "${reset:+ (↻${reset})}" "${RST}"
}
# Join non-empty segments with two spaces (escapes stay literal for final %b)
join_segs() {
local out="" s
for s in "$@"; do
[[ -n "${s}" ]] || continue
out+="${out:+ }${s}"
done
printf '%s' "${out}"
}
# --- Colors (ANSI-256) ---
RST='\e[0m'
DIR_CLR='\e[38;5;117m'
GIT_CLR='\e[38;5;150m'
MODEL_CLR='\e[38;5;147m'
VER_CLR='\e[38;5;249m'
CTX_CLR=$(pct_color "${used_pct}" '\e[38;5;158m' '\e[38;5;215m' '\e[38;5;203m')
# --- Assemble (empty segments are dropped by join_segs; two-space separator) ---
seg_branch=""; [[ -n "${branch}" ]] && seg_branch="${GIT_CLR}🌿 ${branch}${git_state}${RST}"
seg_ver=""; [[ -n "${version}" ]] && seg_ver="${VER_CLR}📟 v${version}${RST}"
seg_sess=""; { [[ "${SHOW_SESSION_ID}" = 1 ]] && [[ -n "${session_id}" ]]; } && seg_sess="${VER_CLR}🔑 ${session_id:0:8}${RST}"
line1=$(join_segs \
"${DIR_CLR}📁 ${short_dir}${RST}" \
"${seg_branch}" \
"${MODEL_CLR}🤖 ${model}${effort:+ (${effort})}${RST}" \
"${seg_ver}" \
"${seg_sess}")
seg_ctx="${CTX_CLR}🧠 Ctx: ${used_pct:-0}%"
(( ${used_tokens:-0} > 0 )) && seg_ctx+=" ($(( (used_tokens + 500) / 1000 ))k)"
seg_ctx+="${RST}"
seg_tok=""; (( ${session_tokens:-0} > 0 )) && seg_tok="${CTX_CLR}🎟️ $(fmt_tokens "${session_tokens}")${RST}"
line2=$(join_segs \
"$(usage_seg "⏳" "5h" "${usage_5h}" "${resets_5h}")" \
"$(usage_seg "📅" "7d" "${usage_7d}" "${resets_7d}")" \
"${seg_ctx}" \
"${seg_tok}")
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