Created
April 11, 2026 00:07
-
-
Save dui/6f387f68e5f4592d5191006189c2e7d4 to your computer and use it in GitHub Desktop.
Claude Code statusline — 2-line context bar with token buckets, 5h/7d rate limits, $ cost breakdown. Pairs with cc_cost.py.
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 | |
| input=$(cat) | |
| # One-shot debug dump: write the latest received JSON so we can verify what | |
| # Claude Code actually sends. Safe to leave on; it's a single file overwrite. | |
| echo "$input" > /tmp/claude-statusline-last.json | |
| # ── Colors ──────────────────────────────────────────────────────── | |
| RESET="\033[0m" | |
| GREEN="\033[38;2;74;222;128m" | |
| YELLOW="\033[33m" | |
| ORANGE="\033[38;5;208m" | |
| RED="\033[31m" | |
| # Context bar icons | |
| C_SYS="\033[38;2;136;136;136m" # Gray — system prompt/tools | |
| C_MSG="\033[38;2;147;51;234m" # Purple — messages | |
| C_FREE="\033[38;2;80;80;80m" # Dim — free space | |
| C_BUF="\033[38;2;100;100;100m" # Dim — autocompact buffer | |
| # Token buckets (per-call) | |
| C_NEW="$GREEN" # Green — uncached input | |
| C_CW="$YELLOW" # Yellow — cache write | |
| C_CR="\033[36m" # Cyan — cache read | |
| C_OUT="\033[34m" # Blue — output | |
| # Chrome | |
| C_DIR="\033[38;2;255;255;255m" # White — directory | |
| C_WT="\033[38;2;255;193;7m" # Gold — worktree tag | |
| C_BR="\033[38;2;78;205;196m" # Teal — git branch | |
| C_SUM="\033[38;2;180;130;255m" # Light purple — session totals | |
| C_ADD="$GREEN" # Green — lines added | |
| C_DEL="$RED" # Red — lines removed | |
| C_DIM="\033[38;2;100;100;100m" # Dim — separators | |
| SEP="${C_DIM}|${RESET}" | |
| # ── Parse JSON (single jq call) ────────────────────────────────── | |
| eval "$(echo "$input" | jq -r ' | |
| @sh "CWD=\(.workspace.current_dir // "?")", | |
| @sh "TRANSCRIPT_PATH=\(.transcript_path // "")", | |
| @sh "CONTEXT_PCT=\(.context_window.used_percentage // 0 | floor)", | |
| @sh "CONTEXT_SIZE=\(.context_window.context_window_size // 200000)", | |
| @sh "DURATION_MS=\(.cost.total_duration_ms // 0)", | |
| @sh "LINES_ADDED=\(.cost.total_lines_added // 0)", | |
| @sh "LINES_REMOVED=\(.cost.total_lines_removed // 0)", | |
| @sh "NEW_TOKENS=\(.context_window.current_usage.input_tokens // 0)", | |
| @sh "CACHE_WRITE=\(.context_window.current_usage.cache_creation_input_tokens // 0)", | |
| @sh "CACHE_READ=\(.context_window.current_usage.cache_read_input_tokens // 0)", | |
| @sh "OUT_TOKENS=\(.context_window.current_usage.output_tokens // 0)", | |
| @sh "TOTAL_IN=\(.context_window.total_input_tokens // 0)", | |
| @sh "TOTAL_OUT=\(.context_window.total_output_tokens // 0)", | |
| @sh "SESSION_COST_USD=\(.cost.total_cost_usd // 0)", | |
| @sh "FIVE_H_PCT=\(.rate_limits.five_hour.used_percentage // empty)", | |
| @sh "FIVE_H_RESET=\(.rate_limits.five_hour.resets_at // empty)", | |
| @sh "SEVEN_D_PCT=\(.rate_limits.seven_day.used_percentage // empty)", | |
| @sh "SEVEN_D_RESET=\(.rate_limits.seven_day.resets_at // empty)" | |
| ')" | |
| # ── Helpers ─────────────────────────────────────────────────────── | |
| fmt() { | |
| local t=$1 | |
| if [ "$t" -ge 1000000 ]; then | |
| awk "BEGIN {printf \"%.1fM\", $t/1000000}" | |
| elif [ "$t" -ge 1000 ]; then | |
| echo "$((t / 1000))k" | |
| else | |
| echo "$t" | |
| fi | |
| } | |
| # 4-tier color ramp: green → yellow → orange → red | |
| ramp() { | |
| local val=$1 t1=$2 t2=$3 t3=$4 | |
| if [ "$val" -lt "$t1" ]; then echo "$GREEN" | |
| elif [ "$val" -lt "$t2" ]; then echo "$YELLOW" | |
| elif [ "$val" -lt "$t3" ]; then echo "$ORANGE" | |
| else echo "$RED" | |
| fi | |
| } | |
| # ── Derived values ──────────────────────────────────────────────── | |
| # Token formatting | |
| NEW_TOK=$(fmt "$NEW_TOKENS") | |
| CW_TOK=$(fmt "$CACHE_WRITE") | |
| CR_TOK=$(fmt "$CACHE_READ") | |
| OUT_TOK=$(fmt "$OUT_TOKENS") | |
| TOT_IN=$(fmt "$TOTAL_IN") | |
| TOT_OUT=$(fmt "$TOTAL_OUT") | |
| # Context window label (200k or 1M) | |
| if [ "$CONTEXT_SIZE" -ge 1000000 ]; then CTX_SIZE="1M" | |
| else CTX_SIZE="$((CONTEXT_SIZE / 1000))k"; fi | |
| # Context breakdown (approximate: sys ~24k fixed, buffer ~16.5% of window) | |
| SYS_OVERHEAD=24000 | |
| COMPACT_BUFFER=$((CONTEXT_SIZE * 165 / 1000)) | |
| TOTAL_INPUT_NOW=$((NEW_TOKENS + CACHE_WRITE + CACHE_READ)) | |
| MSG_TOKENS=$((TOTAL_INPUT_NOW - SYS_OVERHEAD)); [ "$MSG_TOKENS" -lt 0 ] && MSG_TOKENS=0 | |
| FREE_TOKENS=$((CONTEXT_SIZE - TOTAL_INPUT_NOW - COMPACT_BUFFER)); [ "$FREE_TOKENS" -lt 0 ] && FREE_TOKENS=0 | |
| SYS_TOK=$(fmt "$SYS_OVERHEAD") | |
| MSG_TOK=$(fmt "$MSG_TOKENS") | |
| FREE_TOK=$(fmt "$FREE_TOKENS") | |
| BUF_TOK=$(fmt "$COMPACT_BUFFER") | |
| # Context bar (20 cells, each = 5% of window) | |
| BAR_WIDTH=20 | |
| SYS_CELLS=$(( (SYS_OVERHEAD * BAR_WIDTH + CONTEXT_SIZE / 2) / CONTEXT_SIZE )) | |
| BUF_CELLS=$(( (COMPACT_BUFFER * BAR_WIDTH + CONTEXT_SIZE / 2) / CONTEXT_SIZE )) | |
| MSG_CELLS=$(( (MSG_TOKENS * BAR_WIDTH + CONTEXT_SIZE / 2) / CONTEXT_SIZE )) | |
| FREE_CELLS=$(( BAR_WIDTH - SYS_CELLS - MSG_CELLS - BUF_CELLS )) | |
| [ "$FREE_CELLS" -lt 0 ] && FREE_CELLS=0 | |
| CTX_BAR="" | |
| for ((i=0; i<SYS_CELLS; i++)); do CTX_BAR+="${C_SYS}⛁"; done | |
| for ((i=0; i<MSG_CELLS; i++)); do CTX_BAR+="${C_MSG}⛁"; done | |
| for ((i=0; i<FREE_CELLS; i++)); do CTX_BAR+="${C_FREE}⛶"; done | |
| for ((i=0; i<BUF_CELLS; i++)); do CTX_BAR+="${C_BUF}⛝"; done | |
| CTX_BAR+="${RESET}" | |
| # Dynamic colors (context %: 50/70/80, time: 60/120/180 mins) | |
| CTX_COLOR=$(ramp "$CONTEXT_PCT" 50 70 80) | |
| SECS=$((DURATION_MS / 1000)) | |
| MINS=$((SECS / 60)) | |
| TIME="${MINS}m$(printf '%02d' $((SECS % 60)))s" | |
| C_TIME=$(ramp "$MINS" 60 120 180) | |
| # Directory + git | |
| DIR_NAME="${CWD##*/}" | |
| BRANCH="" | |
| if git rev-parse --git-dir > /dev/null 2>&1; then | |
| BRANCH=$(git branch --show-current 2>/dev/null) | |
| GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) | |
| if [ -f "$CWD/.git" ] || [[ "$GIT_DIR" == *"/worktrees/"* ]]; then | |
| DIR_NAME+="${C_WT}[wt]${RESET}" | |
| fi | |
| fi | |
| # ── Daily Target (from cached stats) ────────────────────────────── | |
| STATS_CACHE="$HOME/.cache/pulse-stats/statusbar.json" | |
| STATS_STALE_S=300 # 5 minutes | |
| DAILY_PROGRESS="" | |
| if [ -f "$STATS_CACHE" ]; then | |
| # Check staleness — refresh if >5min old or wrong date | |
| CACHE_MTIME=$(stat -f %m "$STATS_CACHE" 2>/dev/null || echo 0) | |
| NOW_EPOCH=$(date +%s) | |
| CACHE_AGE=$(( NOW_EPOCH - CACHE_MTIME )) | |
| CACHE_DATE=$(jq -r '.today.date // ""' "$STATS_CACHE" 2>/dev/null) | |
| TODAY_DATE=$(date +%Y-%m-%d) | |
| if [ "$CACHE_DATE" != "$TODAY_DATE" ] || [ "$CACHE_AGE" -gt "$STATS_STALE_S" ]; then | |
| python3 "$HOME/.claude/skills/pulse/stats.py" --statusbar &>/dev/null & | |
| fi | |
| # Read cache values | |
| eval "$(jq -r ' | |
| @sh "DT_LINES=\(.today.lines_added // 0)", | |
| @sh "DT_TARGET=\(.target.daily_lines // 5000)", | |
| @sh "DT_PCT=\(.target.progress_pct // 0)" | |
| ' "$STATS_CACHE" 2>/dev/null)" | |
| # Format lines added (e.g., 2.1k) | |
| if [ "${DT_LINES:-0}" -ge 1000 ]; then | |
| DT_FMT=$(awk "BEGIN {printf \"%.1fk\", ${DT_LINES}/1000}") | |
| else | |
| DT_FMT="${DT_LINES:-0}" | |
| fi | |
| # Format target (e.g., 5k) | |
| if [ "${DT_TARGET:-0}" -ge 1000 ]; then | |
| DT_TGT_FMT=$(awk "BEGIN {printf \"%.0fk\", ${DT_TARGET}/1000}") | |
| else | |
| DT_TGT_FMT="${DT_TARGET:-0}" | |
| fi | |
| # Color based on progress (inverted: higher = better) | |
| DT_PCT_VAL="${DT_PCT:-0}" | |
| if [ "$DT_PCT_VAL" -ge 100 ]; then | |
| C_PROG="$GREEN" | |
| elif [ "$DT_PCT_VAL" -ge 50 ]; then | |
| C_PROG="$YELLOW" | |
| elif [ "$DT_PCT_VAL" -ge 25 ]; then | |
| C_PROG="$ORANGE" | |
| else | |
| C_PROG="$RED" | |
| fi | |
| DAILY_PROGRESS="${C_PROG}${DT_FMT}/${DT_TGT_FMT}${RESET}" | |
| fi | |
| # ── cc_cost cache: window + weekly $ totals ────────────────────── | |
| # Refreshed in background by `cc_cost.py --cache-statusbar` (~5s). | |
| # Read instantly from JSON; trigger refresh if stale or missing. | |
| CC_COST_CACHE="$HOME/.cache/claude-stats/cc_cost.json" | |
| CC_COST_STALE_S=300 # 5 minutes | |
| WINDOW_COST_USD="" | |
| WINDOW_CW_USD=""; WINDOW_CR_USD=""; WINDOW_OUT_USD="" | |
| WEEKLY_COST_USD="" | |
| WEEKLY_CW_USD=""; WEEKLY_CR_USD=""; WEEKLY_OUT_USD="" | |
| if [ -f "$CC_COST_CACHE" ]; then | |
| CC_COST_MTIME=$(stat -f %m "$CC_COST_CACHE" 2>/dev/null || echo 0) | |
| NOW_EPOCH=$(date +%s) | |
| CC_COST_AGE=$(( NOW_EPOCH - CC_COST_MTIME )) | |
| if [ "$CC_COST_AGE" -gt "$CC_COST_STALE_S" ]; then | |
| python3 "$HOME/Notes/scripts/cc_cost.py" --cache-statusbar &>/dev/null & | |
| fi | |
| eval "$(jq -r ' | |
| @sh "WINDOW_COST_USD=\(.window_cost_usd // empty)", | |
| @sh "WINDOW_CW_USD=\(.window_cw_usd // empty)", | |
| @sh "WINDOW_CR_USD=\(.window_cr_usd // empty)", | |
| @sh "WINDOW_OUT_USD=\(.window_out_usd // empty)", | |
| @sh "WEEKLY_COST_USD=\(.weekly_cost_usd // empty)", | |
| @sh "WEEKLY_CW_USD=\(.weekly_cw_usd // empty)", | |
| @sh "WEEKLY_CR_USD=\(.weekly_cr_usd // empty)", | |
| @sh "WEEKLY_OUT_USD=\(.weekly_out_usd // empty)" | |
| ' "$CC_COST_CACHE" 2>/dev/null)" | |
| else | |
| # First run: kick off the build so it's available next render. | |
| python3 "$HOME/Notes/scripts/cc_cost.py" --cache-statusbar &>/dev/null & | |
| fi | |
| # Build a [cw cr out] bracket from three USD values. Empty if any missing. | |
| build_breakdown() { | |
| local cw="$1" cr="$2" out="$3" | |
| [ -z "$cw" ] || [ -z "$cr" ] || [ -z "$out" ] && return | |
| local cw_fmt cr_fmt out_fmt | |
| cw_fmt=$(fmt_usd "$cw") | |
| cr_fmt=$(fmt_usd "$cr") | |
| out_fmt=$(fmt_usd "$out") | |
| printf " %b[%b%b%s%b %b%s%b %b%s%b%b]%b" \ | |
| "$C_DIM" "$RESET" "$C_CW" "$cw_fmt" "$RESET" \ | |
| "$C_CR" "$cr_fmt" "$RESET" \ | |
| "$C_OUT" "$out_fmt" "$RESET" "$C_DIM" "$RESET" | |
| } | |
| # Compact $ formatter: $X.XX (<10), $X (10-999), $X.Xk (>=1000) | |
| fmt_usd() { | |
| awk "BEGIN { | |
| v = $1 | |
| if (v >= 1000) printf \"\$%.1fk\", v/1000 | |
| else if (v >= 10) printf \"\$%d\", v | |
| else printf \"\$%.2f\", v | |
| }" | |
| } | |
| # ── Cost & rate limits (from authoritative Claude Code JSON) ───── | |
| # These come straight from Claude Code (no JSONL scanning needed): | |
| # - cost.total_cost_usd : actual session cost | |
| # - rate_limits.five_hour.* : official 5h cap % + reset epoch | |
| # - rate_limits.seven_day.* : official weekly cap % + reset epoch | |
| COST_BLOCK="" | |
| SESSION_COST_FMT=$(awk "BEGIN {printf \"%.2f\", ${SESSION_COST_USD:-0}}") | |
| # 5-hour rate limit block (only present for Pro/Max after first API response) | |
| FIVE_H_BLOCK="" | |
| if [ -n "$FIVE_H_PCT" ]; then | |
| FIVE_H_INT=$(awk "BEGIN {printf \"%d\", ${FIVE_H_PCT:-0}}") | |
| C_5H=$(ramp "$FIVE_H_INT" 25 50 75) | |
| if [ -n "$FIVE_H_RESET" ]; then | |
| NOW_EPOCH=$(date +%s) | |
| REM_5H_S=$(( FIVE_H_RESET - NOW_EPOCH )) | |
| [ "$REM_5H_S" -lt 0 ] && REM_5H_S=0 | |
| REM_5H_H=$(( REM_5H_S / 3600 )) | |
| REM_5H_M=$(( (REM_5H_S % 3600) / 60 )) | |
| if [ "$REM_5H_H" -gt 0 ]; then | |
| REM_5H_FMT="${REM_5H_H}h${REM_5H_M}m" | |
| else | |
| REM_5H_FMT="${REM_5H_M}m" | |
| fi | |
| FIVE_H_BLOCK=" ${C_5H}${FIVE_H_INT}%${RESET}${C_DIM}/5h·${REM_5H_FMT}${RESET}" | |
| else | |
| FIVE_H_BLOCK=" ${C_5H}${FIVE_H_INT}%${RESET}${C_DIM}/5h${RESET}" | |
| fi | |
| if [ -n "$WINDOW_COST_USD" ]; then | |
| WINDOW_FMT=$(fmt_usd "$WINDOW_COST_USD") | |
| WINDOW_BREAKDOWN=$(build_breakdown "$WINDOW_CW_USD" "$WINDOW_CR_USD" "$WINDOW_OUT_USD") | |
| FIVE_H_BLOCK="${FIVE_H_BLOCK}${C_DIM}·${RESET}${C_5H}${WINDOW_FMT}${RESET}${WINDOW_BREAKDOWN}" | |
| fi | |
| fi | |
| # 7-day rate limit block | |
| SEVEN_D_BLOCK="" | |
| if [ -n "$SEVEN_D_PCT" ]; then | |
| SEVEN_D_INT=$(awk "BEGIN {printf \"%d\", ${SEVEN_D_PCT:-0}}") | |
| C_7D=$(ramp "$SEVEN_D_INT" 25 50 75) | |
| if [ -n "$SEVEN_D_RESET" ]; then | |
| NOW_EPOCH=$(date +%s) | |
| REM_7D_S=$(( SEVEN_D_RESET - NOW_EPOCH )) | |
| [ "$REM_7D_S" -lt 0 ] && REM_7D_S=0 | |
| REM_7D_D=$(( REM_7D_S / 86400 )) | |
| REM_7D_H=$(( (REM_7D_S % 86400) / 3600 )) | |
| if [ "$REM_7D_D" -gt 0 ]; then | |
| REM_7D_FMT="${REM_7D_D}d${REM_7D_H}h" | |
| else | |
| REM_7D_FMT="${REM_7D_H}h" | |
| fi | |
| SEVEN_D_BLOCK=" ${C_7D}${SEVEN_D_INT}%${RESET}${C_DIM}/7d·${REM_7D_FMT}${RESET}" | |
| else | |
| SEVEN_D_BLOCK=" ${C_7D}${SEVEN_D_INT}%${RESET}${C_DIM}/7d${RESET}" | |
| fi | |
| if [ -n "$WEEKLY_COST_USD" ]; then | |
| WEEKLY_FMT=$(fmt_usd "$WEEKLY_COST_USD") | |
| WEEKLY_BREAKDOWN=$(build_breakdown "$WEEKLY_CW_USD" "$WEEKLY_CR_USD" "$WEEKLY_OUT_USD") | |
| SEVEN_D_BLOCK="${SEVEN_D_BLOCK}${C_DIM}·${RESET}${C_7D}${WEEKLY_FMT}${RESET}${WEEKLY_BREAKDOWN}" | |
| fi | |
| fi | |
| # Color session cost by absolute amount (rough thresholds) | |
| C_SESS_COST="$GREEN" | |
| SESS_INT=$(awk "BEGIN {printf \"%d\", ${SESSION_COST_USD:-0}}") | |
| if [ "$SESS_INT" -ge 50 ]; then C_SESS_COST="$RED" | |
| elif [ "$SESS_INT" -ge 20 ]; then C_SESS_COST="$ORANGE" | |
| elif [ "$SESS_INT" -ge 10 ]; then C_SESS_COST="$YELLOW" | |
| fi | |
| # Per-bucket session breakdown [cw, cr, out] from cc_cost.py | |
| # Scoped to the current Claude Code process invocation: filter by process | |
| # start (now − DURATION_MS) so the bracket aligns with cost.total_cost_usd, | |
| # which also resets on /compact and on session resume. ~50ms wall. | |
| BREAKDOWN_BLOCK="" | |
| if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && awk "BEGIN {exit !(${SESSION_COST_USD:-0} >= 0.10)}"; then | |
| NOW_EPOCH=$(date +%s) | |
| PROC_START_EPOCH=$(( NOW_EPOCH - DURATION_MS / 1000 - 30 )) # 30s safety buffer | |
| BREAK_OUT=$(python3 "$HOME/Notes/scripts/cc_cost.py" --session-breakdown "$TRANSCRIPT_PATH" "$PROC_START_EPOCH" 2>/dev/null) | |
| if [ -n "$BREAK_OUT" ]; then | |
| eval "$BREAK_OUT" | |
| if awk "BEGIN {exit !(${TOTAL_USD:-0} > 0)}"; then | |
| CW_FMT=$(awk "BEGIN {printf \"%.2f\", ${CW_USD}}") | |
| CR_FMT=$(awk "BEGIN {printf \"%.2f\", ${CR_USD}}") | |
| OUT_FMT=$(awk "BEGIN {printf \"%.2f\", ${OUT_USD}}") | |
| BREAKDOWN_BLOCK=" ${C_DIM}[${RESET}${C_CW}\$${CW_FMT}${RESET} ${C_CR}\$${CR_FMT}${RESET} ${C_OUT}\$${OUT_FMT}${RESET}${C_DIM}]${RESET}" | |
| fi | |
| fi | |
| fi | |
| COST_BLOCK="${C_SESS_COST}\$${SESSION_COST_FMT}${RESET}${BREAKDOWN_BLOCK}${FIVE_H_BLOCK}${SEVEN_D_BLOCK}" | |
| # ── Output (two lines) ──────────────────────────────────────────── | |
| # Line 1: identity + context + per-call tokens + session totals + lines | |
| # Line 2: [daily progress] + cost + time | |
| echo -ne "\ | |
| ${C_DIR}${DIR_NAME}${RESET}$([ -n "$BRANCH" ] && echo " ${SEP} ${C_BR}${BRANCH}${RESET}") ${SEP} \ | |
| ${CTX_BAR} ${CTX_COLOR}${CONTEXT_PCT}%${RESET} $(fmt "$TOTAL_INPUT_NOW")/${CTX_SIZE} \ | |
| ${C_SYS}⛁${RESET}${SYS_TOK} \ | |
| ${C_MSG}⛁${RESET}${MSG_TOK} \ | |
| ${C_FREE}⛶${RESET}${FREE_TOK} \ | |
| ${C_BUF}⛝${RESET}${BUF_TOK} ${SEP} \ | |
| ${C_NEW}•${RESET}${NEW_TOK} \ | |
| ${C_CW}✎${RESET}${CW_TOK} \ | |
| ${C_CR}↺${RESET}${CR_TOK} \ | |
| ${C_OUT}▶${RESET}${OUT_TOK} ${SEP} \ | |
| ${C_SUM}Σ↓${RESET}${TOT_IN} ${C_SUM}↑${RESET}${TOT_OUT} ${SEP} \ | |
| ${C_ADD}+${LINES_ADDED}${RESET}${C_DEL}/-${LINES_REMOVED}${RESET}\n\ | |
| ${DAILY_PROGRESS:+${DAILY_PROGRESS} ${SEP} }${COST_BLOCK} ${SEP} ${C_TIME}${TIME}${RESET}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment