Always-On Cost and Plan-Quota Visibility for Claude Code: Building a Statusline
Values you can pull straight from stdin
Every time Claude Code launches the statusline script, it streams JSON to stdin. Almost everything you need is already there.
# ~/.claude/scripts/statusline.sh
INPUT="$(cat 2>/dev/null || echo '{}')"
j() { printf '%s' "$INPUT" | jq -r "$1" 2>/dev/null; }
CTX=$(j '.context_window.used_percentage // empty')
H5=$(j '.rate_limits.five_hour.used_percentage // empty')
H5R=$(j '.rate_limits.five_hour.resets_at // empty')
D7=$(j '.rate_limits.seven_day.used_percentage // empty')
D7R=$(j '.rate_limits.seven_day.resets_at // empty')
SESSION_USD=$(j '.cost.total_cost_usd // 0')
.context_window.used_percentage is a value Claude Code precomputes and hands you, so you don't have to count tokens yourself. rate_limits only appears after a subscription user has received their first API response, so without the // empty fallback you'll get an error right at the start of a session.
rate_limits only appears "after the first API response, and only on a subscription." Right after startup, fall back to a -- display; the numbers start showing once the first turn comes back.
Reset times arrive as epoch seconds, so convert them to local time with date.
clk() { [ -n "$1" ] && date -r "$1" "+%H:%M" 2>/dev/null; } # → HH:MM
mdhm() { [ -n "$1" ] && date -r "$1" "+%-m/%-d %H:%M" 2>/dev/null; } # → M/D HH:MM
Three-line layout
The output is a fixed three-line structure:
- line1: 📁 dir ⌥ branch
- line2: 🤖 model·effort 🧠 ctx% 🕐 5h N% ⏪HH:MM 📅 7d N% ⏪M/D HH:MM
- line3: 💴 session ≈¥x 今日 ≈¥y <health>
The actual format calls look like this:
L2=$(printf "${MAG}🤖 %s%s${R} ${DIM}·${R} ${CTXC}🧠 ctx %s${R} ${DIM}·${R} ${C5}🕐 5h %s${R}%b ${DIM}·${R} ${C7}📅 7d %s${R}%b" \
"$MODEL" "$EFF" "$(pct "$CTX")" "$(pct "$H5")" "$R5" "$(pct "$D7")" "$R7")
L3=$(printf "${DIM}💴 session${R} %s ${DIM}今日${R} %s %s%b" \
"$(yen "$SESSION_USD")" "$(yen "$DAY_USD")" "$HEALTH" "$PROJ_HEALTH_SEG")
The percentages are color-coded, so you can read the state without reading the numbers.
# pct -> color (green <50, yellow 50-79, red >=80)
pcolor() {
local n="${1%%.*}"; [ -z "$n" ] && { printf '%s' "$DIM"; return; }
if [ "$n" -ge 80 ] 2>/dev/null; then printf '%s' "$RED"
elif [ "$n" -ge 50 ] 2>/dev/null; then printf '%s' "$YEL"
else printf '%s' "$GRN"; fi;
}
Getting today's cost with a 2-minute cache
SESSION_USD comes straight from stdin in real time, but "today's total cost" requires ccusage's daily aggregation. Calling it every time would stall the statusline render, so I use a 2-minute cache plus a background refresh.
CCUSAGE=$(command -v ccusage 2>/dev/null || echo "$HOME/.nvm/versions/node/v24.13.0/bin/ccusage")
DAY_CACHE="/tmp/cc-daily-cache.json"
NEED=true
if [ -f "$DAY_CACHE" ]; then
AGE=$(( $(date +%s) - $(stat -f %m "$DAY_CACHE" 2>/dev/null || echo 0) ))
[ "$AGE" -lt 120 ] && NEED=false
fi
[ "$NEED" = true ] && (
"$CCUSAGE" daily --json 2>/dev/null > "${DAY_CACHE}.tmp" && mv "${DAY_CACHE}.tmp" "$DAY_CACHE"
) &
DAY_USD=0
if [ -f "$DAY_CACHE" ]; then
TODAY=$(date "+%Y-%m-%d")
DAY_USD=$(jq -r --arg d "$TODAY" '(.daily[] | select(.date==$d) | .totalCost) // (.daily[-1].totalCost) // 0' "$DAY_CACHE" 2>/dev/null)
fi
The key point is throwing it into a subshell with &. Even when the cache has expired, the statusline returns the old value at that instant and runs ccusage in the background. A number that's 2 minutes stale causes no practical problems in real use.
The yen conversion is a one-line awk using a rate set at the top.
JPY_RATE=150 # Edit to taste / use $ instead.
yen() {
awk -v u="$1" -v r="$JPY_RATE" 'BEGIN{
v=u*r; n=sprintf("%.0f", v); s=""; len=length(n); c=0
for(i=len;i>=1;i--){ s=substr(n,i,1) s; c++; if(c%3==0 && i>1) s="," s }
printf "¥%s", s
}'
}
It displays with comma separators every three digits, like ¥1,234. If you prefer dollars, just change the spots where yen is called to use $SESSION_USD directly.
A one-character health indicator (5-minute cache)
At the end of L3, I show automation liveness as a single 🟢/🟡/🔴/⚫ character. Because automation-health.sh is expensive to launch, this one uses a 5-minute cache.
HEALTH_CACHE="/tmp/cc-health-cache"; HEALTH=""
if [ -f "$HEALTH_CACHE" ]; then
HAGE=$(( $(date +%s) - $(stat -f %m "$HEALTH_CACHE" 2>/dev/null || echo 0) ))
[ "$HAGE" -lt 300 ] && HEALTH=$(cat "$HEALTH_CACHE" 2>/dev/null)
fi
if [ -z "$HEALTH" ]; then
(
h=$(~/.claude/scripts/automation-health.sh 2>&1 | grep -oE "ALL GREEN|WARN|FAIL" | head -1)
case "$h" in
"ALL GREEN") echo "🟢" > "$HEALTH_CACHE" ;;
"WARN") echo "🟡" > "$HEALTH_CACHE" ;;
"FAIL") echo "🔴" > "$HEALTH_CACHE" ;;
*) echo "⚫" > "$HEALTH_CACHE" ;;
esac
) &
HEALTH="·"
fi
When the cache has expired, it returns · (a dot) while kicking off a background refresh. If the cache is ready by the next render cycle, it switches to the emoji. The statusline render must never block under any circumstances - that's the core of the design.
token-budget-advisor.sh: precise cross-validation
The statusline display is meant to visualize "roughly where I am right now." For autopilot to decide whether to "run or stop," you need precision, and that role falls to token-budget-advisor.sh. It prefers ccusage's official block aggregation, but also computes its own aggregation from ~/.claude/logs/cost-log.jsonl, calculating both and emitting the difference rate (source_diff_pct).
# ccusage が居れば5hブロックのoutput tokenを取る(transcript計算より公式)
if command -v ccusage > /dev/null 2>&1; then
CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
# ... activeブロックを抽出してoutputTokens/costUSDを取り出す
CC_OUTPUT_TOK="${EXTRACTED%|*}"
CC_COST_5H="${EXTRACTED#*|}"
fi
The decision thresholds are set like this:
THRESH_5H_WARN=800_000 # output tokens → warn
THRESH_5H_CRIT=1_200_000 # → critical
THRESH_WEEK_WARN=3000 # USD / 7d
THRESH_SESS_PER_DAY=5 # 直近3d平均 > 5 → burst
In --short mode it produces one-line output, which autopilot reads to make its call ahead of time.
# autopilot側のコードより(前作で紹介)
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
log "ABORT: budget critical"; exit 0
fi
In the autopilot post I described an incident where "a negative remaining slipped through because only the label was checked." For that reason, token-budget-advisor.sh recomputes the remaining amount numerically, separately from the label, and stops if it's 0 or below - a double check.
cost-summary.sh: aggregation by period
~/.claude/scripts/cost-summary.sh # 7日分(デフォルト)
~/.claude/scripts/cost-summary.sh 30 # 30日分
~/.claude/scripts/cost-summary.sh today # 今日だけ
per day: visualizes usage with a Unicode bar graph.
bar = "█" * min(40, int(d["cost"] * 4))
print(f"{day}: ${d['cost']:5.2f} ({d['n']}s) {bar}")
per model: also breaks down message counts by model. I use it to look back on "what did I do on days with lots of Sonnet."
Pitfalls I hit
rate_limitsdoesn't exist right after a session starts → fall back with// empty. A--display is normal until the first turn returnsccusage dailyblocks the statusline → 120-second cache plus&to make it async. Even when stale, the render never stops- The health check is heavy every time → 5-minute cache plus
&. While refreshing, it just returns· - I didn't notice the divergence between
ccusageand my own aggregation → I emitsource_diff_pctso cross-validation is always possible stat -f %mis macOS-only → on Linux, change it tostat -c %Y. The script is written assuming macOS- I mistakenly thought there was no
rate_limitsand tried to hit an endpoint → putting a line that dumps the stdin JSON to a debug log (/tmp/cc-statusline-last.json) in from the start helps you catch this sooner
# デバッグ用。消してもよい
printf '%s' "$INPUT" > /tmp/cc-statusline-last.json 2>/dev/null || true
Wrap-up
- Claude Code's statusline script receives
.rate_limits.*/.context_window.used_percentage/.cost.total_cost_usdon stdin. No auth needed - Three lines give you an always-on view of "branch / 5h·7d·ctx% / session + today's cost in yen plus health"
- Today's cost uses a 2-minute cache plus background refresh so it never blocks
- The one-character health indicator is lightened with a 5-minute cache
token-budget-advisor.shcross-validatesccusage's official values against its own aggregation and feeds autopilot's run/stop decision
Next up, building on this statusline and autopilot's monitoring, I'm writing about having Claude Code patrol GitHub every morning to automatically score useful OSS and skills - Having Claude Code patrol GitHub every morning - auto-scoring useful OSS and skills.
Written by Lily - I ship iOS apps and automate my content stack with Claude Code. Follow along: Portfolio · X · GitHub
Comments
No comments yet. Start the discussion.