はじめに
Claude Codeには特定のタイミングで処理を自動実行する「フック(Lifecycle Hooks)」機能が備わっています。しかし、利便性を求めてフックに複雑な自動化を詰め込みすぎると、意図しないレート制限(Quota)の消費や、運用コストの増大を招くリスクがあります。
本記事では、2026年8月に直面したレート制限到達のトラブル事例をもとに、フック内でのLLM呼び出しを排除し、処理を「自動」から「手動」へ適切に分離することでシステムを安定させる最適化手法を、シニア・デブオプスエンジニアの視点で解説します。
結論
本記事の要点は以下の4点です。
- フック実行=サブセッション起動の罠を理解する: 同期実行されるフックは、処理内容に関わらず新たなサブセッションを立ち上げる。これによりトークン量(Token Quota)だけでなく「セッションレート予算(Session Rate Budget)」を激しく消費する。
- フック内でのLLM呼び出しを排除する: セッション終了時の自動要約を廃止し、純粋なテキスト抽出(JSONLトランスクリプトの取得)に変更することで、APIコストをゼロにする。
- 一旦フックトリガー自体を無効化する: サブプロセス起動そのもののオーバーヘッドを避けるため、頻発するフック(SessionEnd, PreCompact)を一旦停止し、レート制限枠を確保する。
- 無料化できたものだけ選んで戻す: 中身が既に無料化されていたSessionEndの自動ログ記録は後日復元し、頻度が読みにくいPreCompactだけは停止のまま据え置く。ナレッジ化(構造化)は引き続き手動バッチとして運用する。
1. フック実行=同期実行の罠
Claude Codeのフック仕様において、最も見落としがちなコストは「セッションの生成頻度」です。
気づいたきっかけ:journal-cleanupによる連鎖発火
2026年8月4日の13時台、わずか3分間に7回という異常な頻度で「セッションフラッシュスクリプト」が実行されました。調査の結果、バックグラウンドで動いていた複数のjournal-cleanupプロセスがほぼ同時に終了したことが引き金となっていました。
セッションレート予算の浪費
SessionEndやPreCompactといったフックが発火すると、たとえスクリプトの処理が軽量であっても、Claude Codeは新たなサブセッションを起動します。この挙動は、単なるトークン消費量とは別の「セッションレート予算(Session Rate Budget)」を消費します。この時は、処理内容がほぼゼロの状態だったにもかかわらず、高頻度のサブプロセス起動だけでレート制限に到達し、メインの対話がブロックされる事態に陥りました。
2. LLM呼び出しをフック内に置いた失敗
2026年8月3日までの初期設計では、セッション終了時にLLMを呼び出して会話を要約し、日次ログファイルへ書き込んでいました。これが運用コスト増大の元凶でした。
対策:LLM呼び出しの排除と生の文脈抽出
APIコスト削減のため、フックから呼び出すスクリプトを「要約」から「生ログ抽出」へ書き換えました。
# 修正前:Claude Agent SDKによるLLM要約(セッションあたり$0.02〜0.05のコスト)
# summary = claude_agent_sdk.query("このセッションを要約してください")
# 修正後:JSONLトランスクリプトの直接抽出(APIコストゼロ)
context = extract_conversation_context()
# 質疑応答をそのまま日次ログへ追記
定量的変化と設計のトレードオフ
この修正により、見かけ上の課金コストはゼロになりました。また、セッション開始時に注入していたナレッジポインタ(knowledge/index.md相当)も、従来の209記事超(約26KB/2万字)から、直近の更新履歴に絞った約2000字程度へ縮小しました。
ただし、この変更には「ログの質」というトレードオフがあります。以前の「整理済み箇条書き」から「生の質疑応答(濃く・未整理)」に変わったため、情報の整理は後工程の手動バッチ(ナレッジ抽出スクリプト等)へ完全に分離しました。
苦い教訓:uvのPATH問題
この改修中、2026年7月3日から約1ヶ月間、セッション記録の大半がサイレントに欠落していたことが発覚しました。原因は、フック内の subprocess.Popen(["uv", ...]) がシェルのPATHを継承せず、~/.local/bin/uv を見つけられなかったことです。現在は shutil.which("uv") による確認と、絶対パスへのフォールバックを実装して防いでいます。
3. トリガー自体を切り離す判断
スクリプト内のLLM呼び出しを消しても、フックが「サブプロセスを起動する」こと自体がレート予算を食い潰すという根本的な問題は残りました。
対策:フック設定の削除
2026年8月4日、ついに~/.claude/settings.json(グローバル設定)から、頻発するトリガー設定そのものを削除する決断を下しました。
// settings.json から以下のエントリを削除(バックアップ作成後)
"SessionEnd": "...",
"PreCompact": "..."
この時点ではSessionStartのみが残り、ログ記録を含む自動化はいったんゼロになりました。
4. 無料化できたものだけを選んで戻す
話はここで終わりません。後日(別セッションでの対応と推測される)、settings.jsonを確認するとSessionEndのみが再登録され、自動ログ記録が復元されていました。PreCompactは登録されないままです。
再登録の正確な日付・判断理由は日次ログを遡っても特定できていません。 ただし理屈としては整合的です。SessionEndは08-03時点で既にLLM呼び出しを除去済みで、全停止の直接の原因(頻発するフック起動)とは別問題として、中身自体はもう安全でした。一方PreCompactはコンパクションのたびに発火し頻度が読みにくいため、無効のまま据え置かれています。
5. 今どう回しているか:選別済みの自動化
08-03の「内部処理のLLM排除」、08-04の「フック自体の無効化」、そしてその後の「SessionEndのみ再有効化」を経て、現在は次の構成に落ち着いています。
現在の運用フロー
- SessionStart(自動): 文脈維持に必要な最小限の情報のみを注入
- SessionEnd(自動・無料化済み): 会話ログの生抽出をdaily logへ自動追記
- PreCompact(無効のまま): コンパクション時の記録は現状カバーされていない
- ナレッジ化・構造化(手動): 記録内容の整理は手動バッチで実行
運用上の注意点
「一度止めたものを無条件に全部戻す」のではなく、コストの主因(LLM呼び出し/頻発するトリガー)のうちどれが解消済みかを個別に判定してから戻す、という選別が効きました。デブオプス的な視点で見れば、自動化の目的は「楽をすること」だけでなく「リソースを予測可能に保つこと」にあります。高頻度でClaude Codeを使い倒すのであれば、フックの中身と発火頻度を切り分けて評価する習慣が有効です。
