前作「レートリミットで止まったセッションを自動再開する」では5hブロックに止められた後の話を書きました。今回は止まる前に気づく仕組みです。
Claude MAXの5hブロックは出力トークンが上限に達した瞬間にセッションが止まります。ステータスラインに 🕐 5h 82% が見えていても、残り18%が何トークン分かは分かりません。**800k近くで「そろそろやばい」、1.2M超で「もう詰んでいる」**という絶対値での境界を検出するのが token-budget-advisor.sh で、この記事ではその設計と dashboard.sh への統合、ステータスラインへの配線を書きます。
困りごと:上限を踏むまで気づけない
Claude Codeのステータスラインには 🕐 5h N% が表示されます。~/.claude/scripts/statusline.sh はこれをClaudeのstdinから直接読んでいます。
H5=$(j '.rate_limits.five_hour.used_percentage // empty')
H5R=$(j '.rate_limits.five_hour.resets_at // empty')
pcolor 関数が50%以上で黄、80%以上で赤に色を変えますが、これはClaudeが計算したパーセンテージであって、絶対トークン数ではありません。80%が何tokに相当するかはMaxプランの契約状態次第で変わり、長い補完を続けると上限に到達して次のターンから突然止まります。
確認するには毎回 ccusage blocks を叩く必要があり、それを忘れて重いタスクを流すと5h枠をきれいに使い切ります。無人ジョブが裏で動いている場合、人間が気づく前に全SKIP状態になる。
token-budget-advisor.sh の設計
~/.claude/scripts/token-budget-advisor.sh のヘッダにある方針がそのまま設計になっています。
# しきい値:
# 5h block: output > 800k → warn (>1.2M で critical)
# weekly: cost > $3000 → warn
# sessions: >5/day → warn (集中作業の疑い)
#
# 失敗時は exit 0 で fail-open (dashboard 統合のため)
# bash 3.2 互換
デュアルソース:ccusage を1次、cost-log.jsonl を2次
まず ccusage blocks --json を試みます。
if command -v ccusage >/dev/null 2>&1; then
CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
if [ -n "$CC_JSON" ]; then
EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
active = [b for b in d.get('blocks', []) if b.get('isActive')]
if active:
b = active[0]
tc = b.get('tokenCounts', {}) or {}
out = int(tc.get('outputTokens', 0))
cost = float(b.get('costUSD', 0))
print(f'{out}|{cost}')
...")
ccusage が居ない環境は cost-log.jsonl だけで回ります。両方ある場合は ccusage の値を優先し、差分を source_diff_pct として記録します。
# ccusage の値が有効ならそちらを優先 (transcript 計算より信頼できる)
own_out_5h = out_5h
if cc_out is not None and cc_out > 0:
out_5h = cc_out
# 比較 (検証用)
diff_pct = None
if cc_out is not None and own_out_5h > 0:
diff_pct = round(abs(cc_out - own_out_5h) / max(cc_out, own_out_5h) * 100, 1)
差が大きい時はcost-log側の集計ロジックに問題がある可能性があり、source_diff_pct が目安になります。
cost-log.jsonl は「最新行のみ採用」しないと数値が2〜3倍になる
cost-log.jsonl は同じ (session_id, transcript) キーで複数行書かれます(セッション途中の累積値が追記される仕様)。全行を合算するとトークンが二重カウントされます。
# cost-log は session_id × transcript ごとに累積値で書かれる仕様。
# 最新行のみ採用するため、(session_id, transcript) で最終行を取り直す。
latest = {}
with open(log_path) as f:
for line in f:
r = json.loads(line)
key = (r.get("session_id", ""), r.get("transcript", ""))
prev = latest.get(key)
if (prev is None) or (t > prev[0]):
latest[key] = (t, r)
最終行だけ採用してから5h/7d窓でフィルタします。これを知らずに全行合算すると、実際の2〜3倍の数値が出ます。
しきい値と判定ロジック
THRESH_5H_WARN = 800_000 # output tokens
THRESH_5H_CRIT = 1_200_000
THRESH_WEEK_WARN = 3000 # USD
THRESH_SESS_PER_DAY = 5
if out_5h >= THRESH_5H_CRIT:
s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
s5 = "warn"
else:
s5 = "ok"
集中作業判定は「直近3日平均 > 5 sess/day」で出します。
recent_days = sorted(by_day.keys())[-3:]
avg_sess = sum(by_day[d] for d in recent_days) / max(1, len(recent_days))
burst = avg_sess > THRESH_SESS_PER_DAY
--short モードの出力フォーマット
_short フィールドは Python 側で生成されます。
"_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",
状態ごとの出力例(実際のトークン数は変わります)。
🟢 OK (5h:234k tok $0.3 / 7d:$2)
🟡 burst (5h:841k tok $1.2 / 7d:$8)
🔴 cap-near (5h:1243k tok $2.1 / 7d:$12)
アイコンだけ grep -qE '🔴|cap-near' できるのでシェルスクリプトのゲートに直接挿せます。エラー時は ⚫ n/a を出して exit 0 で返るため(fail-open 設計)、呼び出し元が止まりません。
ステータスラインへの埋め込みと dashboard.sh 統合
dashboard.sh の Cost セクションへ組み込む
~/.claude/scripts/dashboard.sh の ## 💰 Cost (7d) セクションがこうなっています。
echo "## 💰 Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo " budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
~/.claude/dashboard.md に書き出されるので、プロジェクト開始時にファイルを開くだけでbadget状況が分かります。
launchd で4時間ごとに自動更新
~/Library/LaunchAgents/com.shun.dashboard.plist の設定です。
<key>Label</key>
<string>com.shun.dashboard</string>
<key>StartInterval</key>
<integer>14400</integer>
14400 秒 = 4時間。macが起きている間は4時間に一度 dashboard.sh が走り、budget 行が自動で書き換わります。手動で ccusage blocks を叩かなくても、ファイルを見れば直近の集計値が確認できます。
無人ジョブのゲートとして使う
autopilotのような無人スクリプトでは --short の出力でジョブを事前停止します(claude-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
ラベル判定だけだと境界付近でギリギリ素通りするケースがあります。クリティカルな判定をする場合は、JSON出力(--short なし)の 5h_output_tokens フィールドを数値で取り出して >= 1200000 を直接チェックする方が確実です。
踏んだ落とし穴
-
cost-log.jsonl を全行合算して数値が2〜3倍になった →
(session_id, transcript)キーで最終行だけ採用する方式に変更 -
ccusage がPATHに居ない環境でスクリプトがそのまま止まった →
command -v ccusageで存在確認し、なければ cost-log.jsonl 単独で動く fallback パスを持つ -
fail-open にしていないと dashboard.sh ごと落ちる → エラー時は exit 0 +
⚫ n/a出力にして、dashboard の他セクションを守る -
launchd の最小PATHで ccusage が見つからない →
ProgramArgumentsを/bin/zsh -c経由にしてzshのパスを引く -
burst フラグが週末の集中作業で常時点灯する → 直近3日平均 > 5 sess/day の判定なので、用途によっては
THRESH_SESS_PER_DAYを引き上げる -
statusline.sh の
rate_limitsが初回ターン前は--になる → Claudeが「サブスクリプション契約者の最初のAPI応答後」にだけ rate_limits をstdinに含めるため。最初のターン後は正常に表示される
まとめ
- 5hブロックの上限は「止まってから」でなく、800k / 1.2M の絶対値で事前に検出する
-
token-budget-advisor.shは ccusage(1次)と cost-log.jsonl(2次)のデュアルソースで集計し、差分をsource_diff_pctで可視化する - cost-log は
(session_id, transcript)キーの最終行だけ採用しないと2〜3倍の誤計算になる -
--shortモードは🟢/🟡/🔴1行に圧縮し、シェルのゲートや dashboard 埋め込みに直接使える -
dashboard.shが launchd の StartInterval 14400(4h)で自動更新し、budget 行が常に最新に保たれる - 失敗時は fail-open(exit 0 / ⚫ n/a)にしておくと、dashboard 全体の落下を防げる
次回は、このダッシュボードが参照する cost-log.jsonl のログローテーションと、集計が重くなる前に古いエントリを圧縮する話を書きます。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 制作物・記事は bokuwalily.com にまとめています🖥️
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!