Skip to content

Instantly share code, notes, and snippets.

@gitawego
Last active May 6, 2026 12:53
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 rate + TTL countdown
# State dir: ~/.claude/statusline-state/
# State file: ~/.claude/statusline-state/<session_hash>.json
# Format: Cache 97% 59:43
#
# ─────────────────────────────────────────────────────────────────────────────
# 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 | feature/petinsurance
# 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.
echo "$INPUT" | jq '.' > /tmp/statusline-last.json 2>/dev/null || true
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=""
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")
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)
if [ -n "$USAGE" ] && [ "$SIGNATURE" != "$STORED_SIG" ]; then
# New response detected — reset timestamp
jq -n \
--arg sig "$SIGNATURE" \
--argjson ts "$NOW" \
--arg hit_rate "${HIT_RATE:-}" \
'{"sig": $sig, "ts": $ts, "hit_rate": $hit_rate}' \
> "$STATE_FILE"
STORED_TS=$NOW
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
RESET="\033[0m"
# ------------------------------------------------------------------
# Cache hit rate color: green >=50%, grey <50%
# ------------------------------------------------------------------
if [ -n "$HIT_RATE" ] && [ "$HIT_RATE" -ge 50 ]; then
CACHE_COLOR="\033[0;32m" # 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
# Determine context window size
CTX_WINDOW=$(echo "$INPUT" | jq -r '
.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 }")
if [ "$CTX_PCT" -ge 95 ]; then
CTX_COLOR="\033[1;31m" # bold red
elif [ "$CTX_PCT" -ge 80 ]; then
CTX_COLOR="\033[0;31m" # red
elif [ "$CTX_PCT" -ge 50 ]; then
CTX_COLOR="\033[0;33m" # yellow
else
CTX_COLOR="\033[0;32m" # 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)
# ------------------------------------------------------------------
RED="\033[0;31m"
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
# ------------------------------------------------------------------
# Git branch segment
# ------------------------------------------------------------------
CWD=$(echo "$INPUT" | jq -r '.cwd // .workspace.current_dir // empty' 2>/dev/null)
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
fi
# ------------------------------------------------------------------
# Rate limit segments (line 2)
# ------------------------------------------------------------------
FIVE_H_PCT=$(echo "$INPUT" | jq -r '
.rate_limits.five_hour.used_percentage //
.usage_limits.five_hour.percent_used //
.session_window.percent_used //
.five_hour_limit.percent_used //
empty' 2>/dev/null)
FIVE_H_RESET=$(echo "$INPUT" | jq -r '
.rate_limits.five_hour.resets_at //
.usage_limits.five_hour.resets_at //
.session_window.resets_at //
.five_hour_limit.resets_at //
empty' 2>/dev/null)
SEVEN_D_PCT=$(echo "$INPUT" | jq -r '
.rate_limits.seven_day.used_percentage //
.usage_limits.seven_day.percent_used //
.weekly_window.percent_used //
.weekly_limit.percent_used //
empty' 2>/dev/null)
SEVEN_D_RESET=$(echo "$INPUT" | jq -r '
.rate_limits.seven_day.resets_at //
.usage_limits.seven_day.resets_at //
.weekly_window.resets_at //
.weekly_limit.resets_at //
empty' 2>/dev/null)
# Format a duration from a reset epoch: "~Xh Ym" or "~Xd Yh"
format_dur_hours() {
local reset_ts="$1"
local now_ts; now_ts=$(date +%s)
local secs=$(( reset_ts - now_ts ))
[ "$secs" -le 0 ] && echo "~0m" && return
local h=$(( secs / 3600 ))
local m=$(( (secs % 3600) / 60 ))
printf "~%dh%02dm" "$h" "$m"
}
format_dur_days() {
local reset_ts="$1"
local now_ts; now_ts=$(date +%s)
local secs=$(( reset_ts - now_ts ))
[ "$secs" -le 0 ] && echo "~0d" && return
local d=$(( secs / 86400 ))
local h=$(( (secs % 86400) / 3600 ))
printf "~%dd%dh" "$d" "$h"
}
FIVE_H_SEG=""
if [ -n "$FIVE_H_PCT" ] && [ "$FIVE_H_PCT" != "null" ]; then
FIVE_H_INT=$(printf "%.0f" "$FIVE_H_PCT" 2>/dev/null)
DUR_STR=""
if [ -n "$FIVE_H_RESET" ] && [ "$FIVE_H_RESET" != "null" ]; then
DUR_STR=" $(format_dur_hours "$FIVE_H_RESET")"
fi
if [ "$FIVE_H_INT" -ge 80 ] 2>/dev/null; then
FIVE_H_SEG="${RED}5h ${FIVE_H_INT}%${DUR_STR}${RESET}"
else
FIVE_H_SEG="5h ${FIVE_H_INT}%${DUR_STR}"
fi
fi
SEVEN_D_SEG=""
if [ -n "$SEVEN_D_PCT" ] && [ "$SEVEN_D_PCT" != "null" ]; then
SEVEN_D_INT=$(printf "%.0f" "$SEVEN_D_PCT" 2>/dev/null)
DUR_STR=""
if [ -n "$SEVEN_D_RESET" ] && [ "$SEVEN_D_RESET" != "null" ]; then
DUR_STR=" $(format_dur_days "$SEVEN_D_RESET")"
fi
if [ "$SEVEN_D_INT" -ge 80 ] 2>/dev/null; then
SEVEN_D_SEG="${RED}7d ${SEVEN_D_INT}%${DUR_STR}${RESET}"
else
SEVEN_D_SEG="7d ${SEVEN_D_INT}%${DUR_STR}"
fi
fi
# ------------------------------------------------------------------
# Assemble line 1: model | ctx N% | effort | Cache N% MM:SS | branch
# ------------------------------------------------------------------
LINE1=""
append_seg() {
local seg="$1"
if [ -n "$LINE1" ]; then
LINE1="${LINE1} | ${seg}"
else
LINE1="${seg}"
fi
}
[ -n "$MODEL_STR" ] && append_seg "$MODEL_STR"
[ -n "$CTX_STR" ] && append_seg "$CTX_STR"
if [ -n "$EFFORT_STR" ] && [ "$EFFORT_STR" != "null" ]; then
append_seg "${RED}${EFFORT_STR}${RESET}"
fi
[ -n "$HIT_RATE" ] && append_seg "${CACHE_COLOR}Cache ${HIT_RATE}%${RESET} ${TTL_COLOR}${TTL_STR}${RESET}"
[ -n "$BRANCH_STR" ] && append_seg "$BRANCH_STR"
# ------------------------------------------------------------------
# Assemble line 2: 5h N% ~dur | 7d N% ~dur (only if data present)
# ------------------------------------------------------------------
LINE2=""
if [ -n "$FIVE_H_SEG" ] || [ -n "$SEVEN_D_SEG" ]; then
if [ -n "$FIVE_H_SEG" ] && [ -n "$SEVEN_D_SEG" ]; then
LINE2="${FIVE_H_SEG} | ${SEVEN_D_SEG}"
elif [ -n "$FIVE_H_SEG" ]; then
LINE2="$FIVE_H_SEG"
else
LINE2="$SEVEN_D_SEG"
fi
fi
# ------------------------------------------------------------------
# Final output
# ------------------------------------------------------------------
if [ -z "$LINE1" ]; then
exit 0
fi
if [ -n "$LINE2" ]; then
printf '%b\n%b' "$LINE1" "$LINE2"
else
printf '%b' "$LINE1"
fi
@gitawego
Copy link
Copy Markdown
Author

gitawego commented May 6, 2026

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