In my last post, "Letting Claude Code improve itself autonomously — autopilot", I covered how to keep autopilot from running away, but I glossed over the part about "surfacing your plan quota in the statusline" in a single paragraph. This time I'll walk through that implementation in full.
No auth, no endpoint calls. All you do is read the JSON that Claude Code passes to the statusline script over stdin with jq, and you get your 5h/7d plan quota, context usage, and session cost plus today's running total (converted to yen) always visible in three lines.
The problem: consumption that surprises you "later"
Claude Max's API has soft rate limits. As your output tokens approach the cap within a 5-hour block or a 7-day window, the model's behavior changes, and once you burn through it, subsequent slots get skipped. The problem is that you usually can't see this consumption.
- "Did the model's replies just get thinner?" → turns out your 5h quota was full
- You start a manual session without realizing autopilot's background usage is being counted in too
- Your sense of cost drifts, and you only grasp the situation at month-end when you check ccusage
The "open a dashboard in a separate tab" approach dies within three days. If the numbers are always on your working screen, the remaining quota catches your eye without you having to think about it.
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_limitsonly 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.shrecomputes 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 returns -
ccusage daily blocks 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 ccusage and my own aggregation → I emit
source_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-validates ccusage'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*
Top comments (0)