Claude Codeのagent hookで「テストが通るまで止めない」を検証させる
Stop hookにtype: agentを指定し、テストが通るまでClaude Codeを止めない設定を作ります。timeoutと50ターンの設計、promptフックとの違い、歯止めの掛け方を整理しました。
Claude CodeのStopフックにtype: "agent"を指定すると、Claudeが応答を終えようとするたびにサブエージェントが起動します。サブエージェントは実際にテストを走らせて結果を読み、ok: trueかok: falseを返します。falseなら理由が次の指示としてClaudeに戻り、作業が続きます。
この方式の主題は「検証のコスト」と「持ち時間の設計」です。判定が賢くなる代わりに、毎回サブエージェントが動く点を先に押さえておきます。なおagentフックは実験的機能で、挙動や設定が将来変わる可能性があると明記されています。
type: "agent"のStopフックは何をするのか
agentフックは、フック入力のJSONとプロンプトを渡されたサブエージェントが、ファイルを読み、コードを検索し、必要なら他のツールも使って条件を確かめる仕組みです。最大50ターンまで調べたあと、{ "ok": true }か{ "ok": false, "reason": "..." }の形で結論を返します。
Stopイベントでの流れは次のとおりです。
agentフックがテスト完了を判定する流れ
- 1
Claudeが応答を終える
Stopフックが発火します。ユーザーの中断では発火しません。 - 2
サブエージェントが起動する
promptと、$ARGUMENTSに展開されたフック入力JSONが渡されます。 - 3
テストを走らせて読む
最大50ターンの範囲で、ファイルの確認やコマンド実行を行います。
- 4
ok を返す
trueならそのまま停止します。falseならreasonがClaudeへの次の指示になり、ターンが続きます。
Stopフックはタスクの完了時だけでなく、Claudeが応答を終えるたびに発火します。「完了した」と判断するのはフック側の仕事です。ユーザーが中断した場合は発火せず、APIエラーで終了した場合はStopFailureが代わりに発火します。
最小の設定 — テストが通るまで止めない
公式ガイドの例は、.claude/settings.jsonに次の形で書きます。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}ポイントは3つです。typeを"agent"にすること、promptに$ARGUMENTSを入れてフック入力を渡すこと、timeoutを秒で明示することです。promptは「全単体テストが通ること」のような条件文でも、「.envを読むBashコマンドを止める」のような規則文でも書けます。
これをテストコマンドが決まっているプロジェクトに合わせると、たとえば次のような形になります。以下は設計の一例で、公式の例そのものではありません。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "このターンでソースやテストのファイルが変更されていなければ {\"ok\": true} を返す。変更されていれば npm test を実行する。全て通れば {\"ok\": true}、失敗があれば失敗したテスト名と最初のエラーを reason に入れて {\"ok\": false, \"reason\": \"...\"} を返す。$ARGUMENTS",
"timeout": 180
}
]
}
]
}
}変更がないターンを即座にtrueで通す一文を足しているのが工夫です。Stopは質問への回答だけで終わるターンでも発火するため、これがないと、コードを触っていないのにテストスイートを毎回走らせることになります。
promptフックとの違いは「判断の材料」と「持ち時間」
同じok/reasonの形式を使うため、promptフックと見分けにくい面があります。違いを並べると次のとおりです。
| 項目 | prompt型 | agent型 |
|---|---|---|
| 判断の材料 | prompt型フック入力と、プロンプトの文面だけ | agent型ファイル・検索・コマンド実行の結果 |
| 呼び出し | prompt型単発のLLM呼び出し | agent型サブエージェント(最大50ターン) |
| 既定のtimeout | prompt型30秒 | agent型60秒 |
impossible | prompt型使える | agent型使えない |
continueOnBlock | prompt型指定できる | agent型指定欄がない |
入力JSONだけで足りる判定、たとえば「last_assistant_messageが完了を宣言しているか」ならpromptフックで済みます。「テストが本当に通るか」はファイルとコマンド結果を見ないと分からないので、agentフックの領分です。
1点、公式の記述にずれがあります。リファレンスは、サブエージェントが使うツールとしてRead・Grep・Globを挙げています。一方、ガイドの例は「テストスイートを実行して」と指示しています。コマンド実行の可否は例の文面から読み取る形になるため、自分の環境で一度動かして、テストが実際に走ることを確かめてから信頼してください。
timeoutと50ターンをどう設計するか
テストを走らせる判定は、時間が読めない作業です。設計の軸は2つあります。
1. timeoutはテストの所要時間に余裕を足す。agent型の既定は60秒で、公式の例でも120秒に伸ばしています。フルテストが1分を超えるプロジェクトでは、既定のままだと判定の途中で打ち切られかねません。一方、command・http・mcp_tool型の超過時の扱いは「出力を捨てる」と定められていますが、prompt・agent型の超過時に何が起きるかは、該当の節に記載がありません。したがって「timeoutを過ぎたら安全側に倒れる」とは期待せず、所要時間に十分な余裕を持たせる設計が無難です。
2. 50ターンを使い切らせない。上限は50ターンですが、テストの実行と結果の読み取りだけなら数ターンで足ります。プロンプトに「失敗が1件でも出たらそこで調査をやめてfalseを返す」と書いておくと、原因調査にターンと時間を費やさずに済みます。原因を探すのは判定役ではなく、reasonを受け取ったClaude本体の仕事です。
検証コストの目安として、使う側が決められるのは次の3点です。
modelフィールド: 既定はバックグラウンド処理用のモデルです。判定に高い能力が要らなければ別のモデルを指定できます- 発火条件: 先ほどの「変更がなければ即
true」のように、プロンプトで早期終了を作ります - 実行範囲: テスト全体ではなく、変更ファイルに関係するテストだけを走らせる指示にします
通らないテストで止まらなくなるのを防ぐ
テストが通らない原因がClaudeの手に負えない場合、ok: falseが返り続けると、Claudeは同じ失敗を抱えて作業を続けようとします。ここには2段の歯止めがあります。
1段目は、impossibleが使えないことへの理解です。promptフックなら、満たせない条件のときにimpossible: trueを付けて停止を許可できます。agentフックはこのフィールドを持たないため、「この条件は満たせない」という脱出口を判定側から出せません。
2段目は、Stopフック共通の連続ブロック上限です。ブロックが連続して8回に達すると、Claude Codeは次のブロックを無効にしてターンを終えます。この回数は、Claudeがツールを呼ぶたびにリセットされます。上限は環境変数CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変更でき、0にすると上限そのものが無効になります。環境変数の詳細はCLAUDE_CODE_STOP_HOOK_BLOCK_CAPの解説にまとめています。
入力JSONのstop_hook_activeも使えます。Claudeがすでにストップフックの結果で続行している最中はtrueになります。プロンプトに「stop_hook_activeがtrueで、前回と同じテストが失敗しているなら、reasonにその旨を明記する」と書いておけば、同じ失敗の繰り返しを人間が気づける形で残せます。これも運用上の工夫の一例で、公式の手順ではありません。
Stop以外のイベントでは挙動が変わる
agentフックはStopだけのものではありません。PreToolUse・PostToolUse・SubagentStop・TaskCompleted・UserPromptSubmitなど12のイベントで使えます。一方、PermissionRequestでは使えません。設定しても無視され、許可の流れは変わりません。
ok: falseのときの扱いも、イベントで異なります。agentフックは、同じイベントでpromptフックにcontinueOnBlock: trueを付けたときと同じ扱いになります。
Stop・SubagentStop:reasonが次の指示としてClaudeに戻り、ターンが続きますPreToolUse: ツール呼び出しが拒否され、reasonがツールエラーとしてClaudeに返ります。ターンは続きますPostToolUse:reasonがClaudeに戻り、ターンが続きますPostToolBatch・UserPromptSubmit・UserPromptExpansion: ターンが終わり、reasonが警告として表示されます
つまりPreToolUseにagentフックを置くと、拒否された操作をClaudeが別の方法で試し直す動きになります。promptフックでは既定でターンが終わるため、同じ「false」でも挙動が逆になります。continueOnBlockを持たないagentフックでは、この選択肢を変えられません。
動作確認の手順
設定を書いたら、実際にフックが動くことを確かめます。
# 設定を保存したあと、Claude Code内で /hooks を開き、
# Stop の下に agent 型のフックが表示されることを確認する
claude設定が反映されない場合は、JSONの末尾カンマやコメントが原因になりがちです。ファイルの編集は通常自動で読み込まれますが、数秒待っても反映されなければセッションを再起動します。
そのうえで、わざと失敗するテストを1件作って次の順に試します。
- Claudeに、そのテストに関係するファイルの小さな修正を頼む
- Claudeが応答を終えた時点で、フックが走っていることを確認する(スピナーに
statusMessageを出したい場合は、フックにstatusMessageを指定します) - 失敗が
reasonとしてClaudeに戻り、作業が続くことを確認する - テストを直して通し、Claudeが停止することを確認する
あわせて、変更のない質問だけのターンでテストが走らないことも確かめてください。毎回走る場合は、プロンプトの早期終了の一文が効いていません。
実験的機能として使うときの線引き
公式は、本番のワークフローではcommandフックを優先するよう案内しています。テストコマンドが決まっていて、成否が終了コードだけで分かるなら、commandフックのほうが速く、timeoutの既定も長く(600秒)、挙動も読みやすい構成です。commandフックでテストを回す具体的な書き方はClaude Code hooksでテストを自動実行するで扱っています。
agentフックが向くのは、次のような場面です。
- モノレポでテストコマンドがパッケージごとに違い、固定のコマンドを書けない
- 失敗したテスト名やエラーを、Claudeが次に動きやすい文面に要約して返したい
- 「テストが通る」に加えて、新しいテストが追加されているかなど、ファイルの内容も見たい
テスト駆動で回す全体の流れはClaude Codeでテスト駆動開発を回す手順にあります。
命令文で書くときの注意
v2.1.294で、prompt・agent型のフックに関する修正が2点入りました。「Block commands that...」のように命令文で書いたフックが、止めるべき操作を許可してしまう不具合の修正が1点目です。2点目は、StopとSubagentStopのprompt型で、命令文の判定が改善され、Claudeが早く止まる可能性が下がった点です。詳しくはv2.1.294のリリースノートを参照してください。
古いバージョンで規則文を使っていた場合は、更新後に拒否が増える可能性があります。プロンプトを「全単体テストが通ること」のような条件文で書くと、判定の意図が曖昧になりにくくなります。