Show original
Enjoyed this article?
Use "Request tipping" to ask the author to set up tip receiving.

AI translation
― statusline implementation
Enjoyed this article?
Use "Request tipping" to ask the author to set up tip receiving.
In the previous work "Claude Codeを無人で自律改善させる ― autopilot", when I wrote countermeasures for autopilot runaway, I glossed over the "display plan slots in statusline" section in a single paragraph. This time I'll explain that implementation in full detail.
No authentication or endpoint calls needed. Claude Code passes JSON to the statusline script via stdin, and by reading it with jq, you can see 5h/7d plan slots, context usage rate, session cost, and today's cumulative total (in yen) constantly displayed in 3 lines.
Claude Max's API has soft rate limits. When output tokens approach the ceiling in 5-hour blocks and 7-day windows, the model's behavior changes, and when exhausted, subsequent slots are skipped. The issue is this consumption status is normally invisible.
Claude Code flows JSON to stdin every time it launches the statusline script. Nearly all necessary values are included 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').contextwindow.usedpercentage is a value pre-calculated by Claude Code itself—you don't need to count tokens yourself. rate_limits only appears after subscription users receive their first API response, so you need to fallback with // empty or you'll get errors right after session start.
rate_limits only appears "after the first API response for subscribers"—a specification. Right after startup it falls back to -- display, and numbers start appearing once the first turn returns.
rate_limits only appears "after the first API response for subscribers"—a specification. Right after startup it falls back to -- display, and numbers start appearing once the first turn returns.
Reset times come in 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:MMOutput is a fixed 3-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 call looks 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")Percentages are color-coded so you can understand the state without reading 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; }SESSION_USD comes in real-time directly from stdin, but "today's total cost" requires ccusage daily aggregation. Calling it every time would clog statusline rendering, so it uses 2-minute caching + background updates.
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)
fiThe key point is passing it to a subshell with &. Even if the cache expires, statusline returns the old value immediately, then runs ccusage in the background. A 2-minute-old number causes no practical issues.
Yen conversion is a one-liner awk 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 }'; }Displays as ¥1,234 with 3-digit comma separators. If you prefer dollar display, just replace the yen call with $SESSION_USD directly.
At the end of L3, a single 🟢/🟡/🔴/⚫ character shows automation liveness. Since automation-health.sh startup cost is high, this uses 5-minute caching.
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="·"
fiWhen cache expires, return · (dot) while starting background updates. If the cache completes by the next render cycle, switch to emoji. Statusline rendering never blocks under any circumstances—that's the core of the design.
Statusline display visualizes "where we are now". To decide whether autopilot "runs or stops" requires precision, and token-budget-advisor.sh handles that role.
Prioritize official ccusage block aggregation while also calculating independent ~/.claude/logs/cost-log.jsonl aggregation and output the difference rate (sourcediffpct).
# 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#*|}"
fiJudgment 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--short mode produces single-line output, which autopilot uses for advance judgment.
# autopilot側のコードより(前作で紹介)
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
log "ABORT: budget critical"; exit 0
fiIn the autopilot article I wrote about an accident where "label judgment alone let negative remaining balance slip through". So token-budget-advisor.sh does double-checking: besides labels, it recalculates remaining balance numerically and stops if it's ≤0.
~/.claude/scripts/cost-summary.sh # 7日分(デフォルト)
~/.claude/scripts/cost-summary.sh 30 # 30日分
~/.claude/scripts/cost-summary.sh today # 今日だけper day: visualizes usage with Unicode bar graphs.
bar = "█" * min(40, int(d["cost"] * 4))
print(f" {day}: ${d['cost']:5.2f} ({d['n']}s) {bar}")per model: also shows message counts by model. I use it to reflect on "what did I do on days with high Sonnet usage?"
# デバッグ用。消してもよい
printf '%s' "$INPUT" > /tmp/cc-statusline-last.json 2>/dev/null || trueNext, I'm writing about having Claude Code make a daily GitHub rounds to auto-score OSS and skills while leveraging this statusline and autopilot monitoring ―― **Claude Codeに毎朝GitHubを巡回させる ― 有用なOSSとスキルを自動採点**.
Lily(@bokuwalily)― Individual developer. Building automation infrastructure with Claude Code while mass-producing iOS apps and web services
◼︎Apps I've made are summarized at **ポートフォリオ**📱
◼︎New releases and development behind-the-scenes on X **@bokuwalily**🐦
◼︎OSS: **github.com/bokuwalily**🐙
Your ❤️ and shares are my motivation!
前作「Claude Codeを無人で自律改善させる ― autopilot」でautopilotの暴走対策を書いたとき、「プラン枠をstatuslineに出す」部分を一段落で流した。今回はその実装を丸ごと解説する。
認証もエンドポイント呼び出しも不要。Claude Codeがstatuslineスクリプトへstdinで渡すJSONを jq で読むだけで、5h/7dプラン枠・コンテキスト使用率・セッションコストと当日累計(円換算)が3行で常時見える。
Claude MaxのAPIにはソフトレート制限がある。5時間ブロックと7日窓で出力トークンが上限に近づくとモデルの挙動が変わり、使い切ると以降のスロットがskipされる。問題はこの消費状況が普段は見えないこと。
「別タブでダッシュボードを開く」運用は三日で廃れる。作業中の画面に常に数字が出ていれば、意識しなくても残量が目に入る。
Claude Codeはstatuslineスクリプトを起動するたびにstdinへJSONを流す。そこに必要な値がほぼ揃っている。
# ~/.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').contextwindow.usedpercentage はClaude Code側が事前計算して渡してくれる値で、自分でトークンを数える必要はない。rate_limits はサブスクリプション契約のユーザーが最初のAPI応答を受け取った後にのみ現れるため、// empty でフォールバックしておかないとセッション開始直後にエラーが出る。
rate_limits が現れるのは「サブスクで最初のAPI応答後にだけ」という仕様がある。開始直後は -- 表示にフォールバックし、最初のターンが返ってきたタイミングで数字が出始める。
rate_limits が現れるのは「サブスクで最初のAPI応答後にだけ」という仕様がある。開始直後は -- 表示にフォールバックし、最初のターンが返ってきたタイミングで数字が出始める。
リセット時刻は epoch 秒で来るので 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出力は固定の3行構成。
line1: 📁 dir ⌥ branch
line2: 🤖 model·effort 🧠 ctx% 🕐 5h N% ⏪HH:MM 📅 7d N% ⏪M/D HH:MM
line3: 💴 session ≈¥x 今日 ≈¥y <health>実際のフォーマット呼び出しはこう。
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")パーセンテージには色がついており、数字を読まなくても状態がわかる。
# 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; }SESSION_USD はstdinから直でリアルタイムに来るが、「今日のトータルコスト」はccusageのdaily集計が必要になる。毎回呼ぶとstatuslineの描画が詰まるので、2分キャッシュ+バックグラウンド更新にしている。
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ポイントは & でサブシェルに投げる点。キャッシュが切れていても、statuslineはその瞬間に古い値を返してから裏でccusageを走らせる。2分古い数字でも実運用上の支障はない。
円換算は先頭で設定したレートのawk1行。
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 }'; }3桁カンマ区切りで ¥1,234 と表示する。ドル表示が好みならyenを呼んでいる箇所をそのまま $SESSION_USD に変えるだけ。
L3の末尾に🟢/🟡/🔴/⚫の1文字でautomationの死活を出している。automation-health.sh の起動コストが高いため、こちらは5分キャッシュ。
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キャッシュが切れているときは ·(ドット)を返しつつバックグラウンドで更新を開始する。次の描画サイクルでキャッシュが完成していれば絵文字に切り替わる。statuslineの描画はいかなる場合もブロックしないのが設計の核。
statuslineの表示は「今どのくらいか」を視覚化するためのもの。autopilotが「走るか・止まるか」を判断するには精度が必要で、その役割は token-budget-advisor.sh が担う。
ccusageの公式ブロック集計を優先しつつ、独自の ~/.claude/logs/cost-log.jsonl 集計と両方計算して差分率(sourcediffpct)を出す。
# 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判定しきい値はこう設定されている。
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--short モードで1行出力になり、autopilotがここを見て先行判断する。
# autopilot側のコードより(前作で紹介)
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
log "ABORT: budget critical"; exit 0
fiautopilot記事で「ラベル判定だけで残量マイナスが素通りした」事故を書いた。そのため token-budget-advisor.sh はラベルとは別に残量を数値で再計算し、0以下なら止める二重チェックになっている。
~/.claude/scripts/cost-summary.sh # 7日分(デフォルト)
~/.claude/scripts/cost-summary.sh 30 # 30日分
~/.claude/scripts/cost-summary.sh today # 今日だけper day: はUnicode棒グラフで使用量を可視化する。
bar = "█" * min(40, int(d["cost"] * 4))
print(f" {day}: ${d['cost']:5.2f} ({d['n']}s) {bar}")per model: でモデル別メッセージ数も出る。「Sonnetが多い日は何をしたか」を振り返るのに使っている。
# デバッグ用。消してもよい
printf '%s' "$INPUT" > /tmp/cc-statusline-last.json 2>/dev/null || true次は、このstatuslineとautopilotの監視を活かしながら Claude Codeに毎朝GitHubを巡回させてOSSとスキルを自動採点する 話 ―― **Claude Codeに毎朝GitHubを巡回させる ― 有用なOSSとスキルを自動採点**を書いています。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
◼︎作ったアプリは **ポートフォリオ** にまとめています📱
◼︎新着・開発の裏側は X **@bokuwalily** で発信しています🐦
◼︎OSS: **github.com/bokuwalily**🐙
皆さんの ❤️ やシェアが励みになります!