AI

Claude Codeを任意の使用率で止めるガードを作っている

  • Claude Code
  • hooks
  • Bash
  • レート制限
  • statusLine

Claude CodeはPro/Maxプランのセッションで、5時間枠と週次枠の消費率を rate_limits としてstatusLineに渡しています。ところが、この数値を読めるのはstatusLineコマンドだけで、実行を止められるhookにはこのデータが渡ってきません。読める場所と止められる場所が分かれているこの非対称を、state fileで橋渡しして任意の使用率でClaude Codeを止めるガードを試作しました。

// statusLineの入力に含まれるrate_limits。公式ドキュメントの例示値
"rate_limits": {
  "five_hour": { "used_percentage": 23.5, "resets_at": 1738425600 },
  "seven_day": { "used_percentage": 41.2, "resets_at": 1738857600 }
}

statusLineのドキュメントによれば、この値は追加のAPI呼び出しなしにstatusLineへ渡ってきます。ここから先の作りを、詰まった順に紹介します。

使用量を自分で止める手段がない

Claude Codeの組み込みの停止は、ウィンドウを使い切る100%の壁だけです。60%まで来たら今日はここで止めておく、週の後半のために予算を残しておく、といった任意の地点で止める手段は用意されていません。

Team・Enterpriseプランではこれがさらに狭くなります。公式サポート記事によれば、usage creditsの購入や組織全体の支出上限の設定ができるのはOwner・Primary Ownerだけです。座席ベースのEnterpriseプランのメンバーは上限に達すると「使用クレジットをリクエストする」リンクが表示されますが、Teamプランのメンバーにはこのリンク自体がありません。Teamプランで超過に気づいた時点でできるのは、管理者に直接頼むことだけです。

自分のペースで作業を配分したい、オンコール当番のために予備を残しておきたい、といった要求は珍しいものではありません。それを自分で設定する一次的な手段が無いので、外側から作ることにしました。

rate_limitsはどこから来るのか

rate_limits の出どころは公式には文書化されていません。AnthropicのRate limitsのAPIリファレンスが挙げているレスポンスヘッダーは anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-*anthropic-priority-* の系列だけで、unified5h を含む名前は出てきません。ユーザー報告では anthropic-ratelimit-unified-5h-* というヘッダー名が挙がっていますが、これは公式文書にない名称で、five_hour(5時間枠)の由来としては辻褄が合う一方、rate_limits にもう1つ含まれる seven_day(週次枠)の由来までは説明できません。確かなのは、Claude Codeがこの数値をセッション中メモリに保持し、statusLineコマンドへの入力に含めて渡しているという観測できる挙動だけです。セッションのトランスクリプトには一切書き出されないので、ローカルのJSONLファイルを集計する既存ツールは実測ではなく推定しかできません。

読めるのはstatusLine、止められるのはhookという非対称

hookはClaude Codeの中で実行を止められる唯一の仕組みですが、hookに渡ってくる入力JSONには rate_limits が含まれていません。共通して入ってくるのは session_idtranscript_pathhook_event_name などで、使用率はそこにありません。

hooksのリファレンスを確認しても、共通の入力フィールドに rate_limits は無く、使用率データが乗るのはstatusLine向けの入力だけです。hookへの公開を求める声は anthropics/claude-code#38380 のようなIssueとして立っていますが、Anthropicからの正式な回答が付いた形跡はなく、動きが無いまま自動でクローズされています。読める場所と止められる場所がドキュメント上ではっきり分かれている以上、両者を橋渡しするのが現実的なルートになります。

既存のツールはこの分かれ目のどちらか片側に立っています。statusLine向けのトラッカーは rate_limits を読んで表示するだけで、claude-code-limiterclauditor のような実行を止めるツールは、本物のウィンドウではなく自前のターン数やトークン数の集計で判定しています。自前集計だと、同じアカウントを複数マシンから使っている分の消費が抜け落ちます。Team・Enterpriseプランで一番困る場面がまさにそこなので、二つの分かれ目のあいだが空いていました。

橋渡しという発想自体には前例があります。Zennの記事は、コンテキスト使用率をstatusLineからファイル経由でUserPromptSubmit hookへ渡し、自動コンパクトの前に警告を出す仕組みを紹介しています。ただしこちらは警告止まりで実行はブロックせず、対象も rate_limits ではなく context_window です。

Claude CodeのメモリからstatusLineとhookの両方へ入力が渡るが、rate_limitsを含むのはstatusLine側だけ。statusLine側のsensorがstate fileへ書き出し、hook側のgateがそれを読んでしきい値と比較する

rate_limits はstatusLineの入力にしか乗らず、hookの入力にはそもそも欄がありません。sensor役がstatusLine側でその値を一度state fileへ落とし、gate役がhook側でそれを読み直すことで、hookの入力に無い情報をhookの判定に使えるようにしています。

state fileで橋渡しする

sensor役のstatusLineスクリプトが rate_limits を毎回state fileへ書き出し、gate役のhookスクリプトがそれを読んで判定します。

# sensor: statusLineの入力からrate_limitsを取り出して書き出す
printf '%s' "$input" | jq -c '{
  five_hour: .rate_limits.five_hour,
  seven_day: .rate_limits.seven_day,
  observed_at: (now | floor)
}' > "$STATE"

gate側は書き出された used_percentage をしきい値と比較し、超えていればブロックします。しきい値は5時間枠・週次枠でそれぞれ独立に設定でき、環境変数が設定ファイルより優先されます。

state fileが古すぎる場合や rate_limits 自体が入っていない場合は、判定せずに通します。claude -p のようにstatusLineが動かない実行ではstate fileが更新されないので、古い数値で止めるより通す方を選びました。ガードの信頼性はstate fileの中身をどこまで信用できるかで決まるので、あいまいな場合は必ず通す側に倒しています。

欠落は例外的な事態ではありません。statusLineのドキュメントは rate_limits について「セッション中の最初のAPIレスポンスより後にのみ現れる」と書いており、さらに five_hourseven_day はそれぞれ独立して欠落しうるとも明記しています。片方だけ入っている状態を想定していない実装だと、セッション開始直後に判定が壊れます。

PreToolUseが要る理由

止める場所として UserPromptSubmit だけでは足りませんでした。このhookはターンの境界でしか発火しないので、一度始まったターンの中は止められません。サブエージェントへのファンアウトや長時間の自律的な作業は、新しいプロンプトを送らないままウィンドウを消費し続けます。

1つのターンの中で、UserPromptSubmitはターン頭に1回だけ発火する。PreToolUseはBashやReadやTaskといったツール呼び出しのたびに発火するので、サブエージェントへのファンアウトを含めてターンの内側で何度も判定できる。走り出したターンの内側では新しいプロンプトが送られないため、UserPromptSubmitの出番がない

PreToolUse はツール呼び出しのたびにターンの内側で発火するので、暴走を実際に食い止められるのはこちらです。パーミッションのドキュメントには「Claude Codeがツール呼び出しを行うとき、PreToolUse hookは権限プロンプトより前に実行される」「exit code 2で終了するhookは、権限ルールが評価される前にツール呼び出しを止める」とあります。bypassPermissions はこのプロンプト自体を省略するモードなので、プロンプトより前に走る PreToolUse はモードに関わらず動きます。エージェントのファンアウトを監視する用途では、こちらのほうが実質的な効き目があります。

使用量を消費しない設計

ガードがブロックするのはAPI呼び出しの前なので、ブロックされたターンはAPIを叩きません。設計上、消費されるトークンはありません。

一つだけ落とし穴があります。UserPromptSubmitUserPromptExpansionSessionStart はhookの標準出力がそのままコンテキストに注入される特別なイベントです。通過するたびに何か出力していると、その分がプロンプトのたびに課金されます。ガードは通過経路で標準出力・標準エラーとも0バイトになるよう作り、テストスクリプトでそれを固定しています。

Teamプランでの検証が残っている

rate_limits が届くことを確認できたのは、いまのところMaxプランのセッションだけです。公式ドキュメントが数値の提供を約束しているのはPro/Maxプランに対してで、動機になったTeamプランのプレミアム座席で同じデータが届くかどうかはまだ確かめていません。

ウィンドウを実際に使い切った状態でガードがどう振る舞うかも、本番のワークロードではまだ試していません。しきい値を下げれば疑似的には再現できますが、本物の消費で枠が詰まった瞬間の挙動は別マシンで検証する必要があります。常用に組み込む前に、この二つは確かめておくつもりです。