Skip to content

Instantly share code, notes, and snippets.

@gitawego
Last active June 12, 2026 13:08
Show Gist options
  • Select an option

  • Save gitawego/ed6c86ca0202879b0e1966696b020788 to your computer and use it in GitHub Desktop.

Select an option

Save gitawego/ed6c86ca0202879b0e1966696b020788 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Claude Code statusline: cache-hit tracking merged with
# https://github.com/danielmackay/claude-code-statusline
#
# Line 1: 🤖 model | 🧠 ctx N% | 💪 effort | ⚡ Cache N% ΣN% MM:SS | 💰 $X.XX
# Cache: last-response hit rate, Σ session-cumulative rate, TTL countdown.
# ↺ (bold red) marks a cache bust — large rebuild after prefix invalidation.
# Line 2: 📁 dir | 🌳 worktree | 🌿 branch +staged ~modified | ⏱️ 5h ███░░░ N% ~dur | 7d ███░░░ N% ~dur
#
# State dir: ~/.claude/statusline-state/<session_hash>.json (cache TTL tracking)
#
# ─────────────────────────────────────────────────────────────────────────────
# MANUAL CONFIGURATION
# ─────────────────────────────────────────────────────────────────────────────
# To wire this script up as your Claude Code statusline, add a `statusLine`
# block to ~/.claude/settings.json (user-level) or .claude/settings.json
# (project-level). Claude Code pipes a JSON payload to stdin on every render
# and displays whatever this script prints to stdout.
#
# 1. Make the script executable:
#
# chmod +x .ps-digital-genai/scripts/statusline-command.sh
#
# 2. Add this block to ~/.claude/settings.json:
#
# {
# "statusLine": {
# "type": "command",
# "command": "/absolute/path/to/.ps-digital-genai/scripts/statusline-command.sh",
# "padding": 0
# }
# }
#
# Use an absolute path — Claude Code does not expand `~` or resolve
# relative paths for statusline commands. For project-local use, point at
# the script inside the project (e.g.
# "$PWD/.ps-digital-genai/scripts/statusline-command.sh").
#
# 3. Reload Claude Code (restart the session or run `/config`). The two-line
# statusline will appear at the bottom of the prompt:
#
# 🤖 opus[1m] | 🧠 ctx 12% | 💪 high | ⚡ Cache 97% 59:43 | 💰 $8.83
# 📁 myrepo | 🌿 feature/petinsurance ~5 | ⏱️ 5h ███░░░ 23% ~3h12m | 7d ██░░░░ 41% ~4d6h
#
# Requirements: `jq`, `awk`, `git`, `md5sum`, `date` (all standard on macOS
# and most Linux distros). The script silently no-ops if input JSON is empty.
#
# To disable temporarily, comment out or remove the `statusLine` block from
# settings.json — no need to delete this file.
# ─────────────────────────────────────────────────────────────────────────────
STATE_DIR="$HOME/.claude/statusline-state"
mkdir -p "$STATE_DIR"
# Read JSON input once
INPUT=$(cat)
# One-shot diagnostic: dump the latest statusline JSON for inspection.
# Overwrites the file on every render so it's never large.
# Write-then-rename so concurrent readers never see a truncated file
{ echo "$INPUT" | jq '.' > "/tmp/statusline-last.json.$$" 2>/dev/null \
&& mv -f "/tmp/statusline-last.json.$$" /tmp/statusline-last.json; } || rm -f "/tmp/statusline-last.json.$$"
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
# Derive a short hash from session_id for the filename
if [ -n "$SESSION_ID" ]; then
SESSION_HASH=$(echo -n "$SESSION_ID" | md5sum | cut -c1-12)
else
SESSION_HASH="default"
fi
STATE_FILE="$STATE_DIR/${SESSION_HASH}.json"
# ------------------------------------------------------------------
# Parse current_usage tokens
# ------------------------------------------------------------------
USAGE=$(echo "$INPUT" | jq -r '.context_window.current_usage // empty')
if [ -n "$USAGE" ]; then
INPUT_TOKENS=$(echo "$USAGE" | jq -r '.input_tokens // 0')
CACHE_CREATE=$(echo "$USAGE" | jq -r '.cache_creation_input_tokens // 0')
CACHE_READ=$(echo "$USAGE" | jq -r '.cache_read_input_tokens // 0')
else
INPUT_TOKENS=0
CACHE_CREATE=0
CACHE_READ=0
fi
# Signature to detect a new response: concatenate the three token counts
SIGNATURE="${INPUT_TOKENS}:${CACHE_CREATE}:${CACHE_READ}"
# ------------------------------------------------------------------
# Load existing state (validate before use)
# ------------------------------------------------------------------
STORED_SIG=""
STORED_TS=0
STORED_HIT_RATE=""
STORED_TOT_READ=0
STORED_TOT_PROMPT=0
if [ -f "$STATE_FILE" ]; then
# Validate: must be a JSON object with the expected keys
VALID=$(jq -e 'type == "object" and has("sig") and has("ts") and has("hit_rate")' "$STATE_FILE" 2>/dev/null)
if [ "$VALID" = "true" ]; then
STORED_SIG=$(jq -r '.sig' "$STATE_FILE")
STORED_TS=$(jq -r '.ts' "$STATE_FILE")
STORED_HIT_RATE=$(jq -r '.hit_rate' "$STATE_FILE")
# Session-cumulative token totals (absent in old-format state files)
STORED_TOT_READ=$(jq -r '.tot_read // 0' "$STATE_FILE")
STORED_TOT_PROMPT=$(jq -r '.tot_prompt // 0' "$STATE_FILE")
fi
fi
# ------------------------------------------------------------------
# Compute cache hit rate
# ------------------------------------------------------------------
TOTAL=$(( INPUT_TOKENS + CACHE_CREATE + CACHE_READ ))
if [ "$TOTAL" -gt 0 ] && [ -n "$USAGE" ]; then
# Use awk for floating-point; round to integer
HIT_RATE=$(awk "BEGIN { printf \"%d\", ($CACHE_READ / $TOTAL) * 100 }")
elif [ -n "$STORED_HIT_RATE" ]; then
# No current_usage yet — fall back to last known hit rate
HIT_RATE="$STORED_HIT_RATE"
else
HIT_RATE=""
fi
# ------------------------------------------------------------------
# Update state only when signature changes (new response)
# ------------------------------------------------------------------
NOW=$(date +%s)
TOT_READ=$STORED_TOT_READ
TOT_PROMPT=$STORED_TOT_PROMPT
if [ -n "$USAGE" ] && [ "$SIGNATURE" != "$STORED_SIG" ]; then
# New response detected — accumulate session totals and update state
TOT_READ=$(( STORED_TOT_READ + CACHE_READ ))
TOT_PROMPT=$(( STORED_TOT_PROMPT + TOTAL ))
jq -n \
--arg sig "$SIGNATURE" \
--argjson ts "$NOW" \
--arg hit_rate "${HIT_RATE:-}" \
--argjson tot_read "$TOT_READ" \
--argjson tot_prompt "$TOT_PROMPT" \
'{"sig": $sig, "ts": $ts, "hit_rate": $hit_rate,
"tot_read": $tot_read, "tot_prompt": $tot_prompt}' \
> "$STATE_FILE"
STORED_TS=$NOW
fi
# Session-cumulative hit rate (all responses seen by this statusline)
CUM_RATE=""
if [ "$TOT_PROMPT" -gt 0 ] 2>/dev/null; then
CUM_RATE=$(awk "BEGIN { printf \"%d\", ($TOT_READ / $TOT_PROMPT) * 100 }")
fi
# Use persisted timestamp if we didn't just update
if [ "$STORED_TS" -gt 0 ] && [ "$SIGNATURE" = "$STORED_SIG" ]; then
REF_TS=$STORED_TS
else
REF_TS=$NOW
fi
# ------------------------------------------------------------------
# TTL countdown: 1 hour from last response
# ------------------------------------------------------------------
TTL=3600
ELAPSED=$(( NOW - REF_TS ))
REMAINING=$(( TTL - ELAPSED ))
if [ "$REMAINING" -le 0 ]; then
TTL_STR="exp"
TTL_COLOR="\033[2;37m" # dim grey — expired
else
MINS=$(( REMAINING / 60 ))
SECS=$(( REMAINING % 60 ))
TTL_STR=$(printf "%d:%02d" "$MINS" "$SECS")
if [ "$REMAINING" -ge 2400 ]; then
# 40-60 min remaining: green
TTL_COLOR="\033[0;32m"
elif [ "$REMAINING" -ge 1200 ]; then
# 20-40 min remaining: yellow
TTL_COLOR="\033[0;33m"
elif [ "$REMAINING" -ge 300 ]; then
# 5-20 min remaining: red
TTL_COLOR="\033[0;31m"
else
# Last 5 min: bold red (terminal will blink if supported)
TTL_COLOR="\033[1;31m"
fi
fi
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
RED="\033[0;31m"
RESET="\033[0m"
# ------------------------------------------------------------------
# Cache hit rate color: green >=50%, grey <50%.
# Bust detection: a near-zero hit rate combined with a large cache write
# means the prompt prefix was invalidated (compaction, model switch, edited
# context) and rebuilt at 1.25x cost — flag it in bold red with ↺.
# ------------------------------------------------------------------
CACHE_BUST=""
if [ -n "$USAGE" ] && [ "$CACHE_CREATE" -ge 20000 ] 2>/dev/null \
&& [ -n "$HIT_RATE" ] && [ "$HIT_RATE" -lt 20 ] 2>/dev/null; then
CACHE_BUST="↺"
fi
if [ -n "$CACHE_BUST" ]; then
CACHE_COLOR="\033[1;31m" # bold red — paid cache rebuild
elif [ -n "$HIT_RATE" ] && [ "$HIT_RATE" -ge 50 ] 2>/dev/null; then
CACHE_COLOR="$GREEN"
else
CACHE_COLOR="\033[2;37m" # dim grey
fi
# ------------------------------------------------------------------
# Context window usage segment (computed live, not persisted)
# ------------------------------------------------------------------
CTX_STR=""
CTX_COLOR=""
if [ -n "$USAGE" ]; then
# Prefer the percentage Claude Code computes itself (accounts for output
# tokens and the session's actual window size, e.g. 1M models).
CTX_PCT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // empty' 2>/dev/null)
CTX_PCT=${CTX_PCT%.*}
if [ -z "$CTX_PCT" ] || [ "$CTX_PCT" = "null" ]; then
# Fallback: compute from token counts against the window size
CTX_WINDOW=$(echo "$INPUT" | jq -r '
.context_window.context_window_size //
.context_window.max_tokens //
.context_window.context_limit //
.model.context_window //
empty' 2>/dev/null)
# Fall back to 200000; use 1000000 for 1M models
if [ -z "$CTX_WINDOW" ] || [ "$CTX_WINDOW" = "null" ]; then
MODEL_ID=$(echo "$INPUT" | jq -r '.model.id // empty')
MODEL_NAME=$(echo "$INPUT" | jq -r '.model.display_name // empty')
if echo "$MODEL_ID$MODEL_NAME" | grep -qiE '1m'; then
CTX_WINDOW=1000000
else
CTX_WINDOW=200000
fi
fi
# Numerator: all tokens in current_usage (output tokens not included — that's correct)
CTX_USED=$(( INPUT_TOKENS + CACHE_CREATE + CACHE_READ ))
CTX_PCT=$(awk "BEGIN { printf \"%d\", ($CTX_USED / $CTX_WINDOW) * 100 }")
fi
if [ "$CTX_PCT" -ge 95 ]; then
CTX_COLOR="\033[1;31m" # bold red
elif [ "$CTX_PCT" -ge 80 ]; then
CTX_COLOR="$RED"
elif [ "$CTX_PCT" -ge 50 ]; then
CTX_COLOR="$YELLOW"
else
CTX_COLOR="$GREEN"
fi
CTX_STR="${CTX_COLOR}ctx ${CTX_PCT}%${RESET}"
fi
# ------------------------------------------------------------------
# Model segment
# ------------------------------------------------------------------
# Read settings.json model value early — used for [1m] detection below
_SETTINGS_MODEL=$(jq -r '.model // empty' "$HOME/.claude/settings.json" 2>/dev/null)
MODEL_STR=$(echo "$INPUT" | jq -r '.model.display_name // .model.id // empty' 2>/dev/null)
if [ -z "$MODEL_STR" ] || [ "$MODEL_STR" = "null" ]; then
MODEL_STR="$_SETTINGS_MODEL"
fi
# Compact model name: "Claude Opus 4.7 (1M context)" → "opus[1m]"
if [ -n "$MODEL_STR" ] && [ "$MODEL_STR" != "null" ]; then
# Detect [1m] from the runtime context window only — reflect the actual
# allocation, not the requested model in settings.json.
_has_1m=0
_CTX_SIZE=$(echo "$INPUT" | jq -r '.context_window.context_window_size // empty' 2>/dev/null)
if [ -n "$_CTX_SIZE" ] && [ "$_CTX_SIZE" != "null" ] && [ "$_CTX_SIZE" -ge 1000000 ] 2>/dev/null; then
_has_1m=1
fi
_compact=$(echo "$MODEL_STR" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/^claude //' \
| sed -E 's/ ?\(1m context\)/[1m]/g' \
| sed -E 's/ [0-9]+\.[0-9]+//' \
| sed -E 's/^[[:space:]]+|[[:space:]]+$//')
# If result still has hyphens (id form), extract family name
if echo "$_compact" | grep -q '-'; then
_family=$(echo "$_compact" | grep -oE 'opus|sonnet|haiku' | head -1)
if [ -n "$_family" ]; then
_compact="$_family"
[ "$_has_1m" -eq 1 ] && _compact="${_compact}[1m]"
fi
fi
# Force [1m] suffix when the user is on the 1M context variant,
# even if Claude Code's display_name didn't include it
if [ "$_has_1m" -eq 1 ] && ! echo "$_compact" | grep -q '\[1m\]'; then
_compact="${_compact}[1m]"
fi
# Fall back to raw id if compact is empty
if [ -z "$_compact" ]; then
_compact=$(echo "$INPUT" | jq -r '.model.id // empty' 2>/dev/null)
fi
MODEL_STR="$_compact"
fi
# ------------------------------------------------------------------
# Effort level segment (red)
# ------------------------------------------------------------------
EFFORT_STR=$(echo "$INPUT" | jq -r '.effort.level // .output_style.name // empty' 2>/dev/null)
if [ -z "$EFFORT_STR" ] || [ "$EFFORT_STR" = "null" ]; then
EFFORT_STR=$(jq -r '.effortLevel // empty' "$HOME/.claude/settings.json" 2>/dev/null)
fi
# ------------------------------------------------------------------
# Cost segment
# ------------------------------------------------------------------
COST_STR=""
TOTAL_COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // empty' 2>/dev/null)
if [ -n "$TOTAL_COST" ] && [ "$TOTAL_COST" != "null" ]; then
COST_STR=$(awk "BEGIN { printf \"\$%.2f\", $TOTAL_COST }")
fi
# ------------------------------------------------------------------
# Directory / worktree / git segments (line 2)
# ------------------------------------------------------------------
CWD=$(echo "$INPUT" | jq -r '.cwd // .workspace.current_dir // empty' 2>/dev/null)
DIR_STR=""
if [ -n "$CWD" ] && [ "$CWD" != "null" ]; then
REPO_ROOT=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null || echo "$CWD")
DIR_STR=$(basename "$REPO_ROOT")
fi
WORKTREE_STR=$(echo "$INPUT" | jq -r '.worktree.name // empty' 2>/dev/null)
[ "$WORKTREE_STR" = "null" ] && WORKTREE_STR=""
BRANCH_STR=""
if [ -n "$CWD" ] && [ "$CWD" != "null" ]; then
BRANCH_STR=$(git -C "$CWD" symbolic-ref --short HEAD 2>/dev/null)
if [ -z "$BRANCH_STR" ]; then
BRANCH_STR=$(git -C "$CWD" rev-parse --short HEAD 2>/dev/null)
fi
# Staged / modified file counts: +N (green), ~N (yellow)
if [ -n "$BRANCH_STR" ]; then
STAGED=$(git -C "$CWD" diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
MODIFIED=$(git -C "$CWD" diff --numstat 2>/dev/null | wc -l | tr -d ' ')
[ "$STAGED" -gt 0 ] 2>/dev/null && BRANCH_STR="${BRANCH_STR} ${GREEN}+${STAGED}${RESET}"
[ "$MODIFIED" -gt 0 ] 2>/dev/null && BRANCH_STR="${BRANCH_STR} ${YELLOW}~${MODIFIED}${RESET}"
fi
fi
# ------------------------------------------------------------------
# Rate limit segments (line 3)
# ------------------------------------------------------------------
FIVE_H_PCT=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.used_percentage // empty' 2>/dev/null)
FIVE_H_RESET=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.resets_at // empty' 2>/dev/null)
SEVEN_D_PCT=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.used_percentage // empty' 2>/dev/null)
SEVEN_D_RESET=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.resets_at // empty' 2>/dev/null)
# Format a duration from a reset epoch: "~XhYYm" or "~XdYh"
format_dur_hours() {
local reset_ts="$1"
local secs=$(( reset_ts - NOW ))
[ "$secs" -le 0 ] && echo "~0m" && return
printf "~%dh%02dm" $(( secs / 3600 )) $(( (secs % 3600) / 60 ))
}
format_dur_days() {
local reset_ts="$1"
local secs=$(( reset_ts - NOW ))
[ "$secs" -le 0 ] && echo "~0d" && return
printf "~%dd%dh" $(( secs / 86400 )) $(( (secs % 86400) / 3600 ))
}
# 6-char progress bar: ███░░░
make_bar() {
local pct="$1" width=6 bar="" i=0
[ "$pct" -gt 100 ] 2>/dev/null && pct=100
local filled=$(( pct * width / 100 ))
while [ $i -lt $filled ]; do bar="${bar}█"; i=$(( i + 1 )); done
while [ $i -lt $width ]; do bar="${bar}░"; i=$(( i + 1 )); done
printf "%s" "$bar"
}
# label, pct, reset_ts, dur_formatter -> colored "label ██████ N% ~dur"
format_rl() {
local label="$1" pct="$2" reset_ts="$3" dur_fn="$4"
[ -z "$pct" ] || [ "$pct" = "null" ] && return
pct=$(printf "%.0f" "$pct" 2>/dev/null) || return
local color="$GREEN"
if [ "$pct" -ge 90 ] 2>/dev/null; then color="$RED"
elif [ "$pct" -ge 70 ] 2>/dev/null; then color="$YELLOW"
fi
local dur=""
if [ -n "$reset_ts" ] && [ "$reset_ts" != "null" ]; then
dur=" $($dur_fn "$reset_ts")"
fi
printf "%b%s %s %s%%%s%b" "$color" "$label" "$(make_bar "$pct")" "$pct" "$dur" "$RESET"
}
FIVE_H_SEG=$(format_rl "5h" "$FIVE_H_PCT" "$FIVE_H_RESET" format_dur_hours)
SEVEN_D_SEG=$(format_rl "7d" "$SEVEN_D_PCT" "$SEVEN_D_RESET" format_dur_days)
# ------------------------------------------------------------------
# Assemble lines
# ------------------------------------------------------------------
join_segs() {
local out="" seg
for seg in "$@"; do
[ -z "$seg" ] && continue
if [ -n "$out" ]; then out="${out} | ${seg}"; else out="$seg"; fi
done
printf '%s' "$out"
}
CACHE_SEG=""
if [ -n "$HIT_RATE" ]; then
CACHE_SEG="${CACHE_COLOR}Cache ${HIT_RATE}%${CACHE_BUST}${RESET}"
# Σ = session-cumulative rate
if [ -n "$CUM_RATE" ]; then
CACHE_SEG="${CACHE_SEG} \033[2;37mΣ${CUM_RATE}%${RESET}"
fi
CACHE_SEG="${CACHE_SEG} ${TTL_COLOR}${TTL_STR}${RESET}"
fi
EFFORT_SEG=""
if [ -n "$EFFORT_STR" ] && [ "$EFFORT_STR" != "null" ]; then
EFFORT_SEG="${RED}${EFFORT_STR}${RESET}"
fi
LINE1=$(join_segs \
"${MODEL_STR:+🤖 $MODEL_STR}" \
"${CTX_STR:+🧠 $CTX_STR}" \
"${EFFORT_SEG:+💪 $EFFORT_SEG}" \
"${CACHE_SEG:+⚡ $CACHE_SEG}" \
"${COST_STR:+💰 $COST_STR}")
LINE2=$(join_segs \
"${DIR_STR:+📁 $DIR_STR}" \
"${WORKTREE_STR:+🌳 $WORKTREE_STR}" \
"${BRANCH_STR:+🌿 $BRANCH_STR}" \
"${FIVE_H_SEG:+⏱️ $FIVE_H_SEG}" \
"$SEVEN_D_SEG")
# ------------------------------------------------------------------
# Final output (skip empty lines)
# ------------------------------------------------------------------
OUT=""
for line in "$LINE1" "$LINE2"; do
[ -z "$line" ] && continue
if [ -n "$OUT" ]; then OUT="${OUT}\n${line}"; else OUT="$line"; fi
done
[ -z "$OUT" ] && exit 0
printf '%b' "$OUT"
@gitawego

gitawego commented May 6, 2026

Copy link
Copy Markdown
Author

prompt:

/statusline 加 Cache 命中率 + TTL 倒计时, 格式参考 Cache 97% 59:43。

  - 命中率:cache_read / (input + cache_creation + cache_read),取自 current_usage。绿 ≥50% / 灰 <50%。可用来识别中转站(长会话跑不高 = 可疑)。
  - TTL:1 小时,从上次响应倒数。settings.json 设 "refreshInterval": 1。
  - 只在新响应时重置:三个 token 数拼 signature,变了才更新时间戳,否则永远卡60:00。
  - 颜色:0-20m 绿、20-40m 黄、40-55m 红、最后 5m 闪红、过期 exp 灰。
  - 多会话隔离:state 文件按 session_id 哈希命名。
  - state 读前校验;首次响应前 fallback 上次命中率。
 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment