Last active
March 31, 2026 21:31
-
-
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
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
| #!/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}" |
Author
Author
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
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 不是注释 — 会意外启动
claudeCLI原脚本第 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不兼容 worktreegit worktree 下
.git是文件不是目录。改为用rev-parse --show-toplevel后此问题一并解决。4. 费用修正逻辑(
$3.75/M补差)前提有误原脚本假设 CC 用
$6.25/M但实际应按$10/M(1h cache)。分析源码后发现:ttl: '1h'),但配额内不扣费,费用只是参考$6.25/M就是正确价格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.branch/worktree.name,显示如fix/issue-123[wt:fix-123]+lines/-lines代码变更、exceeds_200k警告(!)、vim mode、session name6d20h而非164h20m↺164hc()/dim()函数 + 颜色常量,消除重复 ANSI 码fmt_duration()复用:duration 和 rate limit 倒计时共用fmt_quota()复用:5h/7d 格式化逻辑合并printf替代echo:避免 trailing newline 在 subshell 嵌套中产生空 segmentSHOW_USER_HOST配置开关:顶部变量控制是否显示user@host: