Skip to content

Instantly share code, notes, and snippets.

@n-WN
Last active March 31, 2026 21:31
Show Gist options
  • Select an option

  • Save n-WN/07abd31ac1a3bc960a542abdebc04303 to your computer and use it in GitHub Desktop.

Select an option

Save n-WN/07abd31ac1a3bc960a542abdebc04303 to your computer and use it in GitHub Desktop.
Claude Code custom statusline — shows rate limits, corrected cost, git branch, session duration
#!/bin/bash
# Claude Code custom statusline
# Shows: user@host:path branch [model] ctx% $cost duration 5h/7d quota Sonnet flag
#
# Setup:
# 1. Save this file to ~/.claude/statusline-command.sh
# 2. Add to ~/.claude/settings.json:
# {
# "statusLine": {
# "type": "command",
# "command": "bash ~/.claude/statusline-command.sh"
# }
# }
# 3. Restart Claude Code
#
# Features:
# - Rate limits (5h/7d) with reset countdown, cached across sessions
# - Session cost corrected for 1h cache pricing (Pro/Max users)
# - Git branch + dirty state
# - Context window usage %
# - Session duration
# - Sonnet model indicator with quota
#
# Requires: jq, git (optional)
# https://gist.github.com/n-WN/07abd31ac1a3bc960a542abdebc04303
input=$(cat)
NOW=$(date +%s)
# ---------- Parse all fields in one jq call ----------
eval "$(echo "$input" | jq -r '
@sh "cwd=\(.workspace.current_dir // .cwd // "")",
@sh "project_dir=\(.workspace.project_dir // .workspace.current_dir // "")",
@sh "model_id=\(.model.id // "")",
@sh "model_raw=\(.model.display_name // "")",
@sh "used=\(.context_window.used_percentage // "")",
@sh "session_id=\(.session_id // "")",
@sh "claude_cost=\(.cost.total_cost_usd // 0)",
@sh "duration_ms=\(.cost.total_duration_ms // 0)",
@sh "rl_5h=\(.rate_limits.five_hour.used_percentage // "")",
@sh "rl_5h_reset=\(.rate_limits.five_hour.resets_at // "")",
@sh "rl_7d=\(.rate_limits.seven_day.used_percentage // "")",
@sh "rl_7d_reset=\(.rate_limits.seven_day.resets_at // "")",
@sh "cache_create=\(.context_window.current_usage.cache_creation_input_tokens // 0)"
')" || exit 0
# ---------- Derived values ----------
cwd="${cwd/#$HOME/\~}"
model="${model_raw/ \(1M context\)/ 1M}"
model="${model/ \(200K context\)/ 200K}"
# ---------- Git branch (single git call) ----------
git_seg=""
if [ -n "$project_dir" ] && [ -d "$project_dir/.git" ]; then
branch=$(git -C "$project_dir" rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$branch" ]; then
if [ -z "$(git -C "$project_dir" status --porcelain 2>/dev/null | head -1)" ]; then
git_seg=" \033[0;32m${branch}\033[0m"
else
git_seg=" \033[0;33m${branch}*\033[0m"
fi
fi
fi
# ---------- Cost (with 1h cache correction, single jq call) ----------
# Claude Code uses $6.25/M for cache writes, but firstParty (Pro/Max) get 1h at $10/M.
# Track cumulative correction by detecting when cache_create changes.
CCOST_FILE="/tmp/claude-ccost-${session_id}"
prev_corr=0
if [ -n "$session_id" ]; then
if [ -f "$CCOST_FILE" ]; then
read -r prev_cc prev_corr < "$CCOST_FILE"
prev_corr="${prev_corr:-0}"
if [ "$cache_create" != "${prev_cc:-}" ] && [ "$cache_create" != "0" ]; then
prev_corr=$(jq -n "$prev_corr + $cache_create * 3.75 / 1000000")
fi
elif [ "$cache_create" != "0" ]; then
prev_corr=$(jq -n "$cache_create * 3.75 / 1000000")
fi
echo "$cache_create $prev_corr" > "$CCOST_FILE"
fi
real_cost=$(jq -n "($claude_cost + $prev_corr) * 100 | round / 100")
cost_seg=""
[ "$real_cost" != "0" ] && cost_seg=" \033[0;90m\$\033[0;33m${real_cost}\033[0m"
# ---------- Duration ----------
duration_s=$(( duration_ms / 1000 ))
dur_h=$(( duration_s / 3600 ))
dur_m=$(( (duration_s % 3600) / 60 ))
dur_seg=""
if [ "$dur_h" -gt 0 ]; then
dur_seg=" \033[0;90m${dur_h}h${dur_m}m\033[0m"
elif [ "$dur_m" -gt 0 ]; then
dur_seg=" \033[0;90m${dur_m}m\033[0m"
fi
# ---------- Rate limits (live → cache fallback) ----------
CACHE_FILE="$HOME/.claude/rate-limits-cache.json"
rl_stale=""
if [ -n "$rl_5h" ] || [ -n "$rl_7d" ]; then
echo "{\"fh\":\"$rl_5h\",\"fr\":\"$rl_5h_reset\",\"sd\":\"$rl_7d\",\"sr\":\"$rl_7d_reset\"}" > "$CACHE_FILE"
elif [ -f "$CACHE_FILE" ]; then
eval "$(jq -r '@sh "rl_5h=\(.fh)",@sh "rl_5h_reset=\(.fr)",@sh "rl_7d=\(.sd)",@sh "rl_7d_reset=\(.sr)"' "$CACHE_FILE" 2>/dev/null)"
[ -n "$rl_5h" ] || [ -n "$rl_7d" ] && rl_stale="*"
fi
fmt_reset() {
[ -z "$1" ] && return
local d=$(( $1 - NOW ))
[ "$d" -le 0 ] && return
local h=$(( d / 3600 )) m=$(( d % 3600 / 60 ))
[ "$h" -gt 0 ] && echo "${h}h${m}m" || echo "${m}m"
}
# ---------- Build segments ----------
quota=""
if [ -n "$rl_5h" ]; then
r=$(fmt_reset "$rl_5h_reset")
quota="$quota \033[0;90m5h:\033[0;35m$(printf '%.0f' "$rl_5h")%${rl_stale}\033[0m"
[ -n "$r" ] && quota="$quota\033[0;90m(↺${r})\033[0m"
else
quota="$quota \033[0;90m5h:\033[0;37m--\033[0m"
fi
if [ -n "$rl_7d" ]; then
r=$(fmt_reset "$rl_7d_reset")
quota="$quota \033[0;90m7d:\033[0;35m$(printf '%.0f' "$rl_7d")%${rl_stale}\033[0m"
[ -n "$r" ] && quota="$quota\033[0;90m(↺${r})\033[0m"
else
quota="$quota \033[0;90m7d:\033[0;37m--\033[0m"
fi
sonnet=""
if [[ "$model_id" == *sonnet* ]] && [ -n "$rl_5h" ]; then
sonnet=" \033[0;33mSonnet\033[0m\033[0;90m[$(printf '%.0f' "$rl_5h")%]\033[0m"
elif [[ "$model_id" == *sonnet* ]]; then
sonnet=" \033[0;33mSonnet\033[0m"
fi
flag=""
FLAG_LOG="/tmp/claude-ctf-flags.log"
[ -f "$FLAG_LOG" ] && [ -s "$FLAG_LOG" ] && flag=" \033[0;90mflag:\033[0;32m$(wc -l < "$FLAG_LOG" | tr -d ' ')\033[0m"
ctx=""
[ -n "$used" ] && ctx=" \033[0;90mctx:${used%.*}%\033[0m"
# ---------- Render ----------
echo -ne "\033[0;32m${USER:-$(whoami)}\033[0m@\033[0;36m${HOSTNAME%%.*}\033[0m:\033[0;34m${cwd}\033[0m${git_seg} \033[0;33m[${model}]\033[0m${ctx}${cost_seg}${dur_seg}${quota}${sonnet}${flag}"
@n-WN
Copy link
Copy Markdown
Author

n-WN commented Mar 31, 2026

Review & Improvements (cross-referenced with Claude Code source)

对照 Claude Code 源码 (components/StatusLine.tsx, utils/modelCost.ts, utils/tokens.ts, services/api/claude.ts) 做了一轮完整分析,发现几个问题并做了改进,分享如下:

Bugs Found

1. Line 1 不是注释 — 会意外启动 claude CLI

原脚本第 1 行是纯文本描述,没有 # 前缀,shebang 在第 3 行。macOS 文件系统大小写不敏感,Claude 会解析到 claude 命令,吞掉 stdin 的 JSON 并产生一段 AI 回复混入 statusline 输出。修复:把 shebang 移到第 1 行。

2. Git 分支检测:project_dir/.git 在子目录启动时失效

CC 的 project_dir = getOriginalCwd()(启动时的 cwd),如果从 git 仓库的子目录启动 CC(如 repo/src/),project_dir/.git 不存在。改用 git -C "$cwd" rev-parse --show-toplevel 从任意子目录找到 git 根目录。

3. .git 检测用 -d 不兼容 worktree

git worktree 下 .git 是文件不是目录。改为用 rev-parse --show-toplevel 后此问题一并解决。

4. 费用修正逻辑($3.75/M 补差)前提有误

原脚本假设 CC 用 $6.25/M 但实际应按 $10/M(1h cache)。分析源码后发现:

  • CC 对 Pro/Max 配额内用户确实用 1h cache (ttl: '1h'),但配额内不扣费,费用只是参考
  • CC 对 超额用户回退到 5m cache,此时 $6.25/M 就是正确价格
  • 所以修正逻辑对超额场景反而是错误的。直接用 CC 原始 total_cost_usd 即可

5. cache_creation_input_tokens 字段名

getCurrentUsage() (tokens.ts) 返回 snake_case,CC 透传到 statusline,所以脚本里的字段名是正确的(虽然 SDK 内部用 camelCase)。

Improvements

  • 路径缩短~/Documents/work/agentflow/agentflow~/Doc…/w/agentflow/agentflow(首级 3 字符+…,中间级首字符,末两级完整)
  • Worktree 支持:读取 CC 传入的 worktree.branch / worktree.name,显示如 fix/issue-123[wt:fix-123]
  • 新增段+lines/-lines 代码变更、exceeds_200k 警告(!)、vim mode、session name
  • 7d 倒计时格式:>24h 显示 6d20h 而非 164h20m
  • 0% 配额不显示倒计时:避免无意义的 ↺164h
  • 间距统一:用数组收集 + join 替代手动拼接,每段恰好一个空格
  • 颜色封装c() / dim() 函数 + 颜色常量,消除重复 ANSI 码
  • fmt_duration() 复用:duration 和 rate limit 倒计时共用
  • fmt_quota() 复用:5h/7d 格式化逻辑合并
  • printf 替代 echo:避免 trailing newline 在 subshell 嵌套中产生空 segment
  • SHOW_USER_HOST 配置开关:顶部变量控制是否显示 user@host:

@n-WN
Copy link
Copy Markdown
Author

n-WN commented Mar 31, 2026

Improved version (v2)

对照 CC 源码分析修正后的完整版本,改动见上条评论。

#!/bin/bash
# Claude Code custom statusline
# Setup: { "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" } }
# Requires: jq, git (optional)
# Based on: https://gist.github.com/n-WN/07abd31ac1a3bc960a542abdebc04303

set -f  # disable globbing for safety
input=$(cat)
NOW=$(date +%s)

# ── Config ──────────────────────────────────────────────────────────
SHOW_USER_HOST=0    # 1 = show user@host:  0 = show path only

# ── Colors ──────────────────────────────────────────────────────────
C_DIM='\033[0;90m'   C_RST='\033[0m'
C_GREEN='\033[0;32m' C_YELLOW='\033[0;33m' C_BLUE='\033[0;34m'
C_CYAN='\033[0;36m'  C_MAGENTA='\033[0;35m' C_RED='\033[0;31m'
C_WHITE='\033[0;37m'

# Wrap text in color: $(c COLOR text)
c() { printf '%s' "${1}${2}${C_RST}"; }
# Wrap text in dim:   $(dim text)
dim() { printf '%s' "${C_DIM}${1}${C_RST}"; }

# ── Parse JSON (single jq call) ────────────────────────────────────
eval "$(echo "$input" | jq -r '
  @sh "cwd=\(.workspace.current_dir // .cwd // "")",
  @sh "model_id=\(.model.id // "")",
  @sh "model_raw=\(.model.display_name // "")",
  @sh "used=\(.context_window.used_percentage // "")",
  @sh "session_name=\(.session_name // "")",
  @sh "claude_cost=\(.cost.total_cost_usd // 0)",
  @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 "exceeds_200k=\(.exceeds_200k_tokens // false)",
  @sh "rl_5h=\(.rate_limits.five_hour.used_percentage // "")",
  @sh "rl_5h_reset=\(.rate_limits.five_hour.resets_at // "")",
  @sh "rl_7d=\(.rate_limits.seven_day.used_percentage // "")",
  @sh "rl_7d_reset=\(.rate_limits.seven_day.resets_at // "")",
  @sh "vim_mode=\(.vim.mode // "")",
  @sh "wt_branch=\(.worktree.branch // "")",
  @sh "wt_name=\(.worktree.name // "")"
')" || exit 0

# ── Helpers ─────────────────────────────────────────────────────────

# Shorten path: ~/Doc…/w/last-two/levels
# home→~, first level 3+…, middle→1 char each, last 2 full
shorten_path() {
    local p="$1"
    [[ "$p" == "$HOME"* ]] && p="~${p#$HOME}"
    local saved_IFS="$IFS"; IFS="/"; set -- $p; IFS="$saved_IFS"
    local n=$#
    if [ "$1" = "~" ] && [ "$n" -gt 4 ]; then
        local first="$2"
        [ "${#first}" -gt 3 ] && first="${first:0:3}"
        local mid="" i
        for (( i=3; i<=n-2; i++ )); do
            eval "local seg=\${$i}"; mid="${mid}/${seg:0:1}"
        done
        eval "echo \"~/\${first}\${mid}/\${$((n-1))}/\${$n}\""
    else
        echo "$p"
    fi
}

# Format duration: seconds → "1d2h" / "3h45m" / "12m"
fmt_duration() {
    local s=$1
    local d=$(( s / 86400 )) h=$(( s % 86400 / 3600 )) m=$(( s % 3600 / 60 ))
    if [ "$d" -gt 0 ]; then   printf '%s' "${d}d${h}h"
    elif [ "$h" -gt 0 ]; then printf '%s' "${h}h${m}m"
    elif [ "$m" -gt 0 ]; then printf '%s' "${m}m"
    fi
}

# Format rate limit: label pct_raw reset_ts → colored segment
fmt_quota() {
    local label=$1 pct_raw=$2 reset_ts=$3
    if [ -z "$pct_raw" ]; then
        printf '%s%s' "$(dim "${label}:")" "$(c "$C_WHITE" "--")"; return
    fi
    local pct=$(printf '%.0f' "$pct_raw")
    local seg="$(dim "${label}:")$(c "$C_MAGENTA" "${pct}%${rl_stale}")"
    if [ -n "$reset_ts" ] && [ "$pct" -gt 0 ] 2>/dev/null; then
        local remain=$(( reset_ts - NOW ))
        [ "$remain" -gt 0 ] && seg="${seg}$(dim "(↺$(fmt_duration "$remain"))")"
    fi
    printf '%s' "$seg"
}

# Append non-empty segment to segs array
seg() { [ -n "$1" ] && segs+=("$1"); }

# ── Build segments ──────────────────────────────────────────────────

# Model name: shorten context label
model="${model_raw/ \(1M context\)/ 1M}"
model="${model/ \(200K context\)/ 200K}"

# Git branch (from worktree data or live git)
git_seg=""
if [ -n "$wt_branch" ]; then
    git_seg="$(c "$C_CYAN" "$wt_branch")$(dim "[wt:${wt_name}]")"
elif [ -n "$cwd" ]; then
    git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
    if [ -n "$git_root" ]; then
        branch=$(git -C "$git_root" rev-parse --abbrev-ref HEAD 2>/dev/null)
        if [ -n "$branch" ]; then
            if git -C "$git_root" diff --quiet HEAD 2>/dev/null \
            && git -C "$git_root" diff --cached --quiet HEAD 2>/dev/null; then
                git_seg="$(c "$C_GREEN" "$branch")"
            else
                git_seg="$(c "$C_YELLOW" "${branch}*")"
            fi
        fi
    fi
fi

# Cost
cost_seg=""
[ "$claude_cost" != "0" ] && cost_seg="$(dim '$')$(c "$C_YELLOW" "$(jq -n "$claude_cost * 100 | round / 100")")"

# Lines changed
lines_seg=""
[ "$lines_added" != "0" ] || [ "$lines_removed" != "0" ] \
    && lines_seg="$(c "$C_GREEN" "+${lines_added}")$(dim "/")$(c "$C_RED" "-${lines_removed}")"

# Duration
dur_seg=""
if [ "$duration_ms" != "0" ]; then
    dur_txt=$(fmt_duration $(( duration_ms / 1000 )))
    [ -n "$dur_txt" ] && dur_seg="$(dim "$dur_txt")"
fi

# Rate limits (live → cache fallback)
CACHE_FILE="$HOME/.claude/rate-limits-cache.json"
rl_stale=""
if [ -n "$rl_5h" ] || [ -n "$rl_7d" ]; then
    printf '{"fh":"%s","fr":"%s","sd":"%s","sr":"%s"}' \
        "$rl_5h" "$rl_5h_reset" "$rl_7d" "$rl_7d_reset" > "$CACHE_FILE"
elif [ -f "$CACHE_FILE" ]; then
    eval "$(jq -r '@sh "rl_5h=\(.fh)",@sh "rl_5h_reset=\(.fr)",@sh "rl_7d=\(.sd)",@sh "rl_7d_reset=\(.sr)"' \
        "$CACHE_FILE" 2>/dev/null)"
    { [ -n "$rl_5h" ] || [ -n "$rl_7d" ]; } && rl_stale="*"
fi

# Context
ctx=""
[ -n "$used" ] && ctx="$(dim "ctx:${used%.*}%")"
[ "$exceeds_200k" = "true" ] && ctx="${ctx}$(c "$C_RED" "!")"

# Sonnet downgrade
sonnet=""
if [[ "$model_id" == *sonnet* ]]; then
    sonnet="$(c "$C_RED" "Sonnet!")"
    [ -n "$rl_5h" ] && sonnet="${sonnet}$(dim "[$(printf '%.0f' "$rl_5h")%]")"
fi

# ── Collect & render ────────────────────────────────────────────────
if [ "$SHOW_USER_HOST" = "1" ]; then
    prefix="$(c "$C_GREEN" "${USER:-$(whoami)}")@$(c "$C_CYAN" "${HOSTNAME%%.*}"):$(c "$C_BLUE" "$(shorten_path "$cwd")")"
else
    prefix="$(c "$C_BLUE" "$(shorten_path "$cwd")")"
fi

segs=()
seg "$git_seg"
segs+=("$(c "$C_YELLOW" "[${model}]")")
seg "$ctx"
seg "$cost_seg"
seg "$lines_seg"
seg "$dur_seg"
seg "$(fmt_quota 5h "$rl_5h" "$rl_5h_reset")"
seg "$(fmt_quota 7d "$rl_7d" "$rl_7d_reset")"
seg "$sonnet"
[ -n "$vim_mode" ]    && seg "$(dim "$vim_mode")"
[ -n "$session_name" ] && seg "$(dim "${session_name}")"
FLAG_LOG="/tmp/claude-ctf-flags.log"
[ -f "$FLAG_LOG" ] && [ -s "$FLAG_LOG" ] \
    && seg "$(dim "flag:")$(c "$C_GREEN" "$(wc -l < "$FLAG_LOG" | tr -d ' ')")"

# Join prefix + segments with single space
out="$prefix"
for s in "${segs[@]}"; do out="${out} ${s}"; done
echo -ne "$out"

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