5時間ブロック上限を超える前に止める ― ccusage × コストログで作るトークン番人
前作「眠ったプラグインを自動で棚卸しする」でプラグイン整理の自動化を書きました。今回はその一段上流 ―― そもそもトークン枠を使い切る前に止める仕組みの話です。
Claude Code の5時間ブロックには出力トークンの上限があり、超えると次のブロックに入るまでレートリミットされます。問題は「超えた後に気づく」設計になっていること。autopilot や夜間バッチが裏で走っている環境では、朝起きたら全スロット SKIP 済み、という事態が現実に起きました。token-budget-advisor.sh はそれを「超える前に止める」ために書いたスクリプトです。
困りごと:制限は超えてから初めて見えた
launchd で複数の自動化ジョブが5時間ブロックをまたいで走ると、どのジョブがどれだけ消費したか追えません。claude -p の出力に token 数は出ないし、Claude Code の UI はセッション中しか見えない。残量を確認する手段がなかったため、上限はいつも事後通知でした。
必要だったのは3つです。
- 現在のアクティブブロックの出力トークン数をリアルタイムで取る
- warn(消費多め)と critical(あと少し)を数値で区別する
- dashboard と daily-brief の1行に収まる形で常時出し続ける
5時間ブロックのしきい値
スクリプト冒頭のコメントに実測ベースで決めたしきい値を書いてあります。
# しきい値:
# 5h block: output > 800k → warn (>1.2M で critical)
# weekly: cost > $3000 → warn
# sessions: >5/day → warn (集中作業の疑い)
800k を warn にしている理由は、ここを超えると重いモデルを長く走らせた場合に1〜2ターンで critical に達するからです。1.2M は「もう重い作業は次ブロックへ」の意思決定ライン。この2段で「警告を無視して走り続けた場合のダメージ」を小さくしています。
ccusage × cost-log.jsonl:二重ソース設計
トークン数の取得元を1つにするのは危険です。ccusage がパスに無い環境、blocks が空の初回ターン、jsonl の書き忘れ ―― どれかが欠けても番人が止まると、ガードが全滅します。そのため ccusage を正とし、cost-log.jsonl をフォールバックにした二重ソース構成にしています。
# ccusage が居れば 5h block の output token を取る (transcript 計算より公式)
CC_OUTPUT_TOK=""
CC_COST_5H=""
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}')
else:
print('|')
except Exception:
print('|')
" 2>/dev/null || echo "|")
CC_OUTPUT_TOK="${EXTRACTED%|*}"
CC_COST_5H="${EXTRACTED#*|}"
fi
fi
ccusage blocks --json は blocks[] 配列を返し、isActive: true のブロックが現在の5時間ウィンドウです。tokenCounts.outputTokens を取り出して判定に使います。
Python 集計側では ccusage の値が有効ならそちらを採用し、cost-log.jsonl との差分を source_diff_pct として記録します。
# ccusage の値が有効ならそちらを優先 (transcript 計算より信頼できる)
own_out_5h = out_5h
if cc_out is not None and cc_out > 0:
out_5h = cc_out
if cc_cost is not None and cc_cost > 0:
cost_5h = cc_cost
# 比較 (検証用)
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)
source_diff_pct が大きい(5%以上)なら cost-log.jsonl の集計ロジックにズレがある合図です。番人自身の精度を番人が検証できる構造になっています。
判定と --short 出力
判定ロジックは3ステータスです。
THRESH_5H_WARN = 800_000 # output tokens
THRESH_5H_CRIT = 1_200_000
THRESH_WEEK_WARN = 3000 # USD
if out_5h >= THRESH_5H_CRIT:
s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
s5 = "warn"
else:
s5 = "ok"
加えて「直近3日の平均セッション数が5/day超」を burst フラグで検出します。短期集中作業で枠を消耗しているパターンは weekly cost が増える前兆なので、別軸で警告を出します。
--short モードの出力がこれです。
🟢 OK (5h:234k tok $1.2 / 7d:$45)
🟡 burst (5h:821k tok $4.1 / 7d:$89)
🔴 cap-near (5h:1234k tok $6.7 / 7d:$122)
コード内では _short キーに格納されています。
"_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",
dashboard と daily-brief への統合
dashboard.sh の Cost セクションはこうなっています。
echo "## 💰 Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo " budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
cost-summary.sh --short が $45.20 / 31 sess (7d) のような累積コストを返し、その直下に budget: として5時間ブロックの現在地を並べます。dashboard を開くたびにコストと残量が両方見えるのがポイントです。
daily-brief.sh は run_to 60 というタイムアウトラッパーで cost-summary.sh 7 を呼び出し、10行分を brief のコストセクションに差し込みます。
echo "## 💰 cost(直近 7d)"
run_to 60 ~/.claude/scripts/cost-summary.sh 7 2>&1 | head -10
run_to は gtimeout --kill-after=15 秒数 のラッパーで、集計スクリプトがハングしてもブリーフ全体を殺さない設計になっています。
token-budget-advisor.shを呼ぶdashboard.sh側にはrun_toを被せていません。advisor 自体が fail-open でexit 0を返す設計のため、ハングが起きにくいからです。ただし python3 が詰まるコーナーケースは残るので、今後run_to 10を被せる予定です。
fail-open 設計の理由
スクリプト先頭に set -u はありますが -e は外してあります。
set -u # -e は外す: fail-open 方針
fail-open ヘルパがこうです。
fail_open() {
if [ "$MODE" = "--short" ]; then
echo "⚫ n/a"
else
printf '{"5h_status":"unknown","weekly_status":"unknown","advice":"%s"}\n' "${1:-no data}"
fi
exit 0
}
[ -f "$LOG" ] || fail_open "cost-log.jsonl not found"
dashboard に組み込まれているスクリプトが exit 1 を返すと、dashboard 生成スクリプト自体が -o pipefail の連鎖で止まります。番人は「番人自身が落ちてダッシュボードを止める」より「⚫ n/a を出して素通りさせる」方が安全です。コストの確認手段が失われるよりも、⚫ n/a を見て「今日は手で確認しよう」と気づける方が運用としてましです。
ccusage のインストール
npm install -g ccusage
# または
npx ccusage blocks --json
ccusage blocks は5時間ブロック単位の使用量を JSON で返します。--json を付けないとターミナル向けの装飾出力になるので注意。isActive: true のブロックが存在しない場合(ブロック切れ直後)は空配列が返り、スクリプトは自動でフォールバックします。
踏んだ落とし穴
- ccusage なしで走らせると cost-log の session 重複カウントがある → 最新行のみ採用するため
(session_id, transcript)をキーにして最終行を取り直すロジックを追加した(スクリプト内latest = {}の二周目ループ) - ラベル(🟡)だけ見て残量がマイナスでも素通りした → autopilot 側で数値チェックを別途追加(
REMAINING=$((800000 - BLOCK_OUT))で<= 0を止める) - ccusage が nvm 管理で launchd の PATH に入っていない →
command -v ccusage >/dev/null 2>&1の分岐で graceful degradation、フォールバックの jsonl 集計が引き継ぐ --shortの⚫ n/aを autopilot が「問題なし」と誤読した → autopilot 側のチェックを「🔴 または critical 含む」の OR に変え、⚫もスキップ対象に追加- by_day の集計に session_id 去重をしていなかった → 同一日に複数行ある session が日別カウントを水増しし、
burstが誤発火した。sess_7d_by_dayで set 去重するよう修正
まとめ
- 5時間ブロックの出力トークンしきい値は warn: 800k / critical: 1.2M
- 取得元は ccusage 優先 + cost-log.jsonl フォールバックの二重ソース。
source_diff_pctで精度を自己検証 --shortモードが 🟢/🟡/🔴 の1行サマリを返し、dashboard と daily-brief に差し込む- fail-open(
exit 0+⚫ n/a)で番人がダッシュボードを殺さない設計 - ラベル判定だけでなく、数値で残量を再計算して止めるのが最終ガード
次回は、このしきい値を超えそうになった時に autopilot が自動でモデルを軽いものに切り替える仕組み ―― effort 可変とモデルルーティングの連携を書く予定です。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!