🛡️ 5時間ブロック上限を超える前に止める ― ccusage × コストログで作るトークン番人 — リーダー×
🛡️

5時間ブロック上限を超える前に止める ― ccusage × コストログで作るトークン番人

#claudecode#自動化#ccusage#個人開発2026-06-30 · 約8

前作「眠ったプラグインを自動で棚卸しする」でプラグイン整理の自動化を書きました。今回はその一段上流 ―― そもそもトークン枠を使い切る前に止める仕組みの話です。

Claude Code の5時間ブロックには出力トークンの上限があり、超えると次のブロックに入るまでレートリミットされます。問題は「超えた後に気づく」設計になっていること。autopilot や夜間バッチが裏で走っている環境では、朝起きたら全スロット SKIP 済み、という事態が現実に起きました。token-budget-advisor.sh はそれを「超える前に止める」ために書いたスクリプトです。

困りごと:制限は超えてから初めて見えた

launchd で複数の自動化ジョブが5時間ブロックをまたいで走ると、どのジョブがどれだけ消費したか追えません。claude -p の出力に token 数は出ないし、Claude Code の UI はセッション中しか見えない。残量を確認する手段がなかったため、上限はいつも事後通知でした。

必要だったのは3つです。

  1. 現在のアクティブブロックの出力トークン数をリアルタイムで取る
  2. warn(消費多め)と critical(あと少し)を数値で区別する
  3. 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 --jsonblocks[] 配列を返し、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.shrun_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_togtimeout --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サービスを量産しています

皆さんの ❤️ やシェアが励みになります!