Last active
June 4, 2026 07:07
-
-
Save jwarkentin/855dc8e92d5144c6966fcaebdafa181d to your computer and use it in GitHub Desktop.
Claude Code status line and related script(s)
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
| #!/usr/bin/env python3 | |
| """Quick cost summary for statusline session state. | |
| Usage: | |
| ./session-cost-summary.py [path-to-state.json] [--top N] | |
| Each session's total cost is `prior` (accumulated before the last update) | |
| plus `last` (the most recent segment). | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| from datetime import datetime | |
| DEFAULT_STATE = os.path.join(os.path.dirname(os.path.abspath(__file__)), | |
| "statusline-session-state.json") | |
| def main(): | |
| p = argparse.ArgumentParser(description="Summarize session costs.") | |
| p.add_argument("state", nargs="?", default=DEFAULT_STATE, | |
| help="Path to statusline-session-state.json") | |
| p.add_argument("--top", type=int, default=5, | |
| help="How many top sessions to list (default 5)") | |
| args = p.parse_args() | |
| try: | |
| with open(args.state) as f: | |
| data = json.load(f) | |
| except (OSError, json.JSONDecodeError) as e: | |
| sys.exit(f"Could not read {args.state}: {e}") | |
| rows = [] | |
| for sid, v in data.items(): | |
| total = v.get("prior", 0) + v.get("last", 0) | |
| rows.append((sid, total, v.get("ts", 0))) | |
| grand = sum(r[1] for r in rows) | |
| active = [r for r in rows if r[1] > 0] | |
| print(f"Sessions: {len(rows)} ({len(active)} with cost)") | |
| print(f"Total cost: ${grand:,.2f}") | |
| if active: | |
| print(f"Average (active): ${grand / len(active):,.2f}") | |
| rows.sort(key=lambda r: r[1], reverse=True) | |
| n = max(0, args.top) | |
| if n: | |
| print(f"\nTop {min(n, len(rows))} sessions:") | |
| for sid, total, ts in rows[:n]: | |
| when = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if ts else "—" | |
| print(f" ${total:>9,.2f} {sid} {when}") | |
| if __name__ == "__main__": | |
| main() |
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
| #!/usr/bin/env bash | |
| # Claude Code status line — two-line layout. | |
| # Line 1: dir · branch │ thinking · model · effort · agents · fast │ duration │ edits │ session | |
| # Line 2: tokens(↑sent ↓recv) · cache% · cost │ context bar (pct, used/size) │ limits (5h/7d + resets) | |
| # Reads Claude Code's Status JSON on stdin (schema v2.1.x). Needs jq + a Nerd Font. | |
| # Cost is cumulative across resumes via ~/.claude/statusline-session-state.json, which | |
| # tracks each session's running total from Claude Code's exact per-run cost (a drop | |
| # signals a resume), seeded once from a per-model list-price estimate of the transcript. | |
| input=$(cat) | |
| j() { printf '%s' "$input" | jq -r "$1" 2>/dev/null; } | |
| # ── 256-colour palette ───────────────────────────────────────────── | |
| RESET='\033[0m'; BOLD='\033[1m'; DIM='\033[2m' | |
| C_SESS='\033[38;5;245m' # grey — session name | |
| C_MODEL='\033[38;5;141m' # violet | |
| C_EFFORT='\033[38;5;180m' # tan | |
| C_THINK='\033[38;5;176m' # orchid | |
| C_FAST='\033[38;5;220m' # yellow — fast mode | |
| C_DIR='\033[38;5;75m' # steel blue | |
| C_BRANCH='\033[38;5;179m' # warm gold | |
| C_EMPTY='\033[38;5;238m' # empty bar | |
| C_PCT='\033[38;5;250m' | |
| C_DUR='\033[38;5;109m' | |
| C_COST='\033[38;5;150m' | |
| C_SENT='\033[38;5;110m' | |
| C_RECV='\033[38;5;150m' | |
| C_TOK='\033[38;5;244m' | |
| C_CACHE='\033[38;5;108m' # sage — cache hit | |
| C_EDIT='\033[38;5;215m' | |
| C_AGENT='\033[38;5;80m' | |
| C_LIMIC='\033[38;5;244m' | |
| C_SEP='\033[38;5;240m' | |
| sev() { [ "$1" -ge 80 ] && printf '\033[38;5;167m' || { [ "$1" -ge 50 ] && printf '\033[38;5;179m' || printf '\033[38;5;77m'; }; } | |
| # ── Nerd Font icons (literal glyphs) ─────────────────────────────── | |
| I_MODEL=''; I_THINK=''; I_FAST=''; I_FOLDER=''; I_BRANCH='' | |
| I_TOK=''; I_CACHE=''; I_TIMER=''; I_COST=''; I_EDIT=''; I_AGENT=''; I_LIMIT=''; SEP='│' | |
| # ── Status JSON fields ───────────────────────────────────────────── | |
| session=$(j '.session_name // empty') | |
| model=$(j '.model.display_name // .model.id // "Claude"') | |
| effort=$(j '.effort.level // empty') | |
| thinking=$(j '.thinking.enabled // false') | |
| fast=$(j '.fast_mode // false') | |
| cwd=$(j '.workspace.current_dir // .cwd // ""') | |
| wt_branch=$(j '.worktree.branch // empty') | |
| cost=$(j '.cost.total_cost_usd // 0') | |
| dur_ms=$(j '.cost.total_duration_ms // 0') | |
| add=$(j '.cost.total_lines_added // 0') | |
| rem=$(j '.cost.total_lines_removed // 0') | |
| transcript=$(j '.transcript_path // ""') | |
| pct=$(j '.context_window.used_percentage // empty') | |
| size=$(j '.context_window.context_window_size // empty') | |
| ctx_used=$(j '.context_window.total_input_tokens // empty') | |
| rl5=$(j '.rate_limits.five_hour.used_percentage // empty') | |
| rl5_reset=$(j '.rate_limits.five_hour.resets_at // empty') | |
| rl7=$(j '.rate_limits.seven_day.used_percentage // empty') | |
| rl7_reset=$(j '.rate_limits.seven_day.resets_at // empty') | |
| # ── Git branch: walk up from cwd reading .git/HEAD directly (no subprocess) ── | |
| branch="$wt_branch" | |
| if [ -z "$branch" ]; then | |
| d="$cwd" | |
| while [ -n "$d" ] && [ "$d" != "/" ]; do | |
| if [ -f "$d/.git/HEAD" ]; then | |
| h=$(cat "$d/.git/HEAD" 2>/dev/null) | |
| case "$h" in | |
| "ref: refs/heads/"*) branch="${h#ref: refs/heads/}" ;; | |
| *) branch=$(printf '%s' "$h" | cut -c1-7) ;; | |
| esac | |
| break | |
| fi | |
| d="${d%/*}" | |
| done | |
| fi | |
| # ── Transcript pass: cumulative sent/received, cache reads, running agents, cost ── | |
| # Cost is summed from token usage × per-model list price; it is cumulative across | |
| # resumes (same transcript file) — unlike the process-local Status cost field. | |
| sent=0; recv=0; cache=0; agents=0; cost_calc=0 | |
| if [ -n "$transcript" ] && [ -f "$transcript" ]; then | |
| read -r sent recv cache agents cost_calc <<EOF | |
| $(jq -rs ' | |
| def rates(m): | |
| if (m|test("opus")) then {i:15, o:75, w:18.75, r:1.5} | |
| elif (m|test("haiku")) then {i:1, o:5, w:1.25, r:0.1} | |
| elif (m|test("sonnet")) then {i:3, o:15, w:3.75, r:0.3} | |
| else {i:3, o:15, w:3.75, r:0.3} end; | |
| def U: map(select(.type=="assistant" and ((.isSidechain//false)|not) and .message.usage!=null) | .message.usage); | |
| [ (U | map((.input_tokens//0)+(.cache_creation_input_tokens//0)+(.cache_read_input_tokens//0)) | add // 0), | |
| (U | map(.output_tokens//0) | add // 0), | |
| (U | map(.cache_read_input_tokens//0) | add // 0), | |
| ( (map(select(.type=="assistant") | .message.content[]? | select(.type=="tool_use" and .name=="Agent") | .id)) as $c | |
| | (map(select(.type=="user") | .message.content[]? | select(.type=="tool_result") | .tool_use_id)) as $d | |
| | [$c[] | select(. as $x | ($d | index($x)) | not)] | length ), | |
| (map(select(.type=="assistant" and .message.usage!=null) | |
| | rates(.message.model // "") as $p | .message.usage | |
| | ((.input_tokens//0)*$p.i + (.cache_creation_input_tokens//0)*$p.w | |
| + (.cache_read_input_tokens//0)*$p.r + (.output_tokens//0)*$p.o) / 1000000) | add // 0) | |
| ] | @tsv' "$transcript" 2>/dev/null) | |
| EOF | |
| fi | |
| [ -z "$agents" ] && agents=0 | |
| # Context fallback for pre-2.1 schemas. | |
| if [ -z "$pct" ]; then | |
| size=200000; [ "$(j '.exceeds_200k_tokens // false')" = "true" ] && size=1000000 | |
| ctx_used=$(jq -rs 'map(select(.type=="assistant" and ((.isSidechain//false)|not) and .message.usage!=null)) | |
| | last | if .==null then 0 else (.message.usage|(.input_tokens//0)+(.cache_creation_input_tokens//0)+(.cache_read_input_tokens//0)) end' "$transcript" 2>/dev/null) | |
| [ -z "$ctx_used" ] && ctx_used=0 | |
| pct=$(( ctx_used * 100 / size )) | |
| fi | |
| # Cache-hit %: cached input ÷ total input sent. | |
| cache_pct="" | |
| [ "${sent:-0}" -gt 0 ] && cache_pct=$(( cache * 100 / sent )) | |
| # ── Cumulative cost across resumes ───────────────────────────────── | |
| # Exact per-run Status cost is accumulated into a shared session-state file, | |
| # ~/.claude/statusline-session-state.json, keyed by session id: { prior, last, ts } | |
| # (extra per-session state can be added alongside these keys later). | |
| # A drop in the Status cost means a resume, so the finished run's cost is folded | |
| # into `prior`. New entries are seeded from the transcript token estimate so | |
| # pre-existing sessions keep their history; deltas after that are exact. The | |
| # read-modify-write is serialised with flock; entries untouched > 6 months are | |
| # pruned on write. Falls back to the estimate when no session id is present. | |
| status_cost="$cost" | |
| sid=$(j '.session_id // empty') | |
| if [ -n "$sid" ]; then | |
| cfile="$HOME/.claude/statusline-session-state.json"; now=$(date +%s); ttl=15552000 # 180 days | |
| exec 9>"$HOME/.claude/.statusline-session-state.lock" 2>/dev/null; flock -w 1 9 2>/dev/null | |
| cur=$(cat "$cfile" 2>/dev/null); [ -z "$cur" ] && cur='{}' | |
| newdoc=$(printf '%s' "$cur" | jq -c \ | |
| --arg sid "$sid" --argjson c "$status_cost" --argjson est "${cost_calc:-0}" \ | |
| --argjson now "$now" --argjson ttl "$ttl" ' | |
| . as $d | |
| | ($d[$sid] // {prior: (if $est-$c>0 then $est-$c else 0 end), last: $c}) as $e | |
| | (if ($c + 1e-7) < ($e.last // 0) then ($e.prior // 0)+($e.last // 0) else ($e.prior // 0) end) as $prior | |
| | $d | .[$sid] = {prior: $prior, last: $c, ts: $now} | |
| | with_entries(select(.value.ts >= ($now - $ttl)))' 2>/dev/null) | |
| if [ -n "$newdoc" ]; then | |
| printf '%s\n' "$newdoc" > "$cfile.tmp" 2>/dev/null && mv -f "$cfile.tmp" "$cfile" 2>/dev/null | |
| cost=$(printf '%s' "$newdoc" | jq -r --arg sid "$sid" --argjson c "$status_cost" '(.[$sid].prior // 0) + $c') | |
| fi | |
| flock -u 9 2>/dev/null; exec 9>&- 2>/dev/null | |
| elif [ -n "$cost_calc" ] && awk -v c="$cost_calc" 'BEGIN{exit !(c>0)}'; then | |
| cost="$cost_calc" | |
| fi | |
| # ── Formatting helpers ───────────────────────────────────────────── | |
| fmt() { awk -v n="${1:-0}" 'BEGIN{ if(n>=1000000){v=n/1000000; printf (v==int(v)?"%dM":"%.1fM"),v} | |
| else if(n>=1000){v=n/1000; printf (v==int(v)?"%dk":"%.1fk"),v} else printf "%d", n }'; } | |
| dur=$(awk -v ms="$dur_ms" 'BEGIN{ s=int(ms/1000); h=int(s/3600); m=int((s%3600)/60); x=s%60; | |
| if(h>0)printf"%dh%dm",h,m; else if(m>0)printf"%dm%ds",m,x; else printf"%ds",x }') | |
| until_reset() { [ -z "$1" ] && return; awk -v t="$1" 'BEGIN{ s=t-systime(); if(s<=0){print"";exit} | |
| dd=int(s/86400); h=int((s%86400)/3600); m=int((s%3600)/60); | |
| if(dd>0)printf"%dd%dh",dd,h; else if(h>0)printf"%dh%dm",h,m; else printf"%dm",(m>0?m:1) }'; } | |
| rl5_in=$(until_reset "$rl5_reset"); rl7_in=$(until_reset "$rl7_reset") | |
| # Context bar (10 cells; fill coloured by severity, empties grey). | |
| filled=$(awk -v p="$pct" 'BEGIN{f=int(p/10+0.5); print (f>10?10:f)}') | |
| bar_f=""; i=0; while [ "$i" -lt "$filled" ]; do bar_f="${bar_f}▓"; i=$((i+1)); done | |
| bar_e=""; while [ "$i" -lt 10 ]; do bar_e="${bar_e}░"; i=$((i+1)); done | |
| CTXC=$(sev "$pct") | |
| cost_h=$(awk -v c="$cost" 'BEGIN{printf "%.2f", c}') | |
| sep=" ${C_SEP}${SEP}${RESET} " | |
| # ── Line 1: dir·branch │ thinking/model/effort/agents/fast │ duration │ edits │ session ── | |
| L1="$(printf "${C_DIR}${BOLD}${I_FOLDER} %s${RESET}" "$(basename "$cwd")")" | |
| [ -n "$branch" ] && L1="$L1$(printf " ${C_BRANCH}${I_BRANCH} %s${RESET}" "$branch")" | |
| L1="$L1${sep}" | |
| [ "$thinking" = "true" ] && L1="$L1$(printf "${C_THINK}${I_THINK}${RESET} ")" | |
| L1="$L1$(printf "${C_MODEL}${BOLD}${I_MODEL} %s${RESET}" "$model")" | |
| [ -n "$effort" ] && L1="$L1$(printf " ${C_EFFORT}%s${RESET}" "$effort")" | |
| [ "${agents:-0}" -gt 0 ] && L1="$L1$(printf " ${C_AGENT}${I_AGENT} %s${RESET}" "$agents")" | |
| [ "$fast" = "true" ] && L1="$L1$(printf " ${C_FAST}${I_FAST}${RESET}")" | |
| L1="$L1${sep}$(printf "${C_DUR}${I_TIMER} %s${RESET}" "$dur")" | |
| if [ "${add:-0}" -gt 0 ] || [ "${rem:-0}" -gt 0 ]; then | |
| L1="$L1${sep}$(printf "${C_EDIT}${I_EDIT} +%s/-%s${RESET}" "$add" "$rem")" | |
| fi | |
| [ -n "$session" ] && L1="$L1${sep}$(printf "${C_SESS}%s${RESET}" "$session")" | |
| # ── Line 2: tokens(↑↓ cache$) │ context bar │ limits ─────────────── | |
| L2="$(printf "${C_TOK}${I_TOK}${RESET} ${C_SENT}↑%s${RESET} ${C_RECV}↓%s${RESET}" "$(fmt "$sent")" "$(fmt "$recv")")" | |
| [ -n "$cache_pct" ] && L2="$L2$(printf " ${C_CACHE}${I_CACHE} %d%%${RESET}" "$cache_pct")" | |
| L2="$L2$(printf " ${C_COST}\$%s${RESET}" "$cost_h")" | |
| L2="$L2${sep}$(printf "${CTXC}%s${C_EMPTY}%s${RESET} ${C_PCT}%d%% %s/%s${RESET}" "$bar_f" "$bar_e" "$pct" "$(fmt "$ctx_used")" "$(fmt "$size")")" | |
| if [ -n "$rl5" ] || [ -n "$rl7" ]; then | |
| L2="$L2${sep}$(printf "${C_LIMIC}${I_LIMIT}${RESET}")" | |
| if [ -n "$rl5" ]; then | |
| L2="$L2$(printf " $(sev "$rl5")5h %d%%${RESET}" "$rl5")" | |
| [ -n "$rl5_in" ] && L2="$L2$(printf " ${C_SEP}↻%s${RESET}" "$rl5_in")" | |
| fi | |
| if [ -n "$rl7" ]; then | |
| L2="$L2$(printf " ${C_SEP}·${RESET} $(sev "$rl7")7d %d%%${RESET}" "$rl7")" | |
| [ -n "$rl7_in" ] && L2="$L2$(printf " ${C_SEP}↻%s${RESET}" "$rl7_in")" | |
| fi | |
| fi | |
| printf '%b\n%b' "$L1" "$L2" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment