Last active
May 6, 2026 12:53
-
-
Save gitawego/ed6c86ca0202879b0e1966696b020788 to your computer and use it in GitHub Desktop.
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 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
prompt: