Stop hookで完了を確かめる
Stop hookの続行が8回続くと、Claude Codeは打ち切ります。テストが通るまで止めないhookと、ループの防ぎ方が分かります。
- Stop hook は Claude が応答を終える直前に動き、ブロックすると理由を添えて作業を続けさせられます。「終わったか」を機械の結果で確かめる場所です。
- Stop は作業の終わりだけでなく応答のたびに動くので、例では src/ と test/ に変更がないときはテストを飛ばしています。
- 最後の応答は transcript_path ではなく last_assistant_message で読みます。ループを防ぐため stop_hook_active が true なら止まらせます。
Stop hook は、Claude が応答を終える直前に動きます。ここでブロックすると、理由を添えて Claude に作業を続けさせられます。「本当に終わったか」を、Claude の自己申告ではなく機械の結果で確かめる場所です。この記事では、渡される JSON、ブロックの返し方、テストが落ちていたら止まらせない hook の例、ループを防ぐ仕組み、SubagentStop との違いを順に説明します。編集部の検証環境(macOS、Claude Code 2.1.306、jq 1.7.1、Node.js 26.8.2、2026年10月22日)で、ダミーの todo アプリにスクリプトを置き、ダミーの JSON を流して確かめました。実際のセッションで発火するところは見ていません(未実行)。完了報告をそのまま信じない理由はAIの「完了しました」を信じない仕組みにまとめています。
Stop がいつ動くか
Stop は、メインの Claude が応答を終えたときに動きます。ユーザーが中断したときは動かず、API のエラーで終わったときは代わりに StopFailure が動きます(出典:https://code.claude.com/docs/en/hooks 、2026-10-22確認)。
Stop は、作業の終わりだけでなく、応答のたびに動きます(出典:https://code.claude.com/docs/en/hooks-guide 、2026-10-22確認)。テストを全部回す Stop hook は、ちょっとした質問に答えたあとにもテストを回します。下の例では、src/ と test/ に変更がなければテストを飛ばしています。
Stop には matcher がありません。書いても無視されます。
渡される JSON
共通のフィールドに加えて、次のフィールドが届きます。
| フィールド | 中身 |
|---|---|
stop_hook_active | Stop hook のせいで、すでに作業を続けている最中なら true |
last_assistant_message | Claude の最後の応答の本文 |
background_tasks | まだ動いているシェルのコマンドやサブエージェントなどの一覧 |
session_crons | このセッションで予定されている起動の一覧 |
最後の応答を読みたいときは、transcript_path のファイルではなく last_assistant_message を使います。公式によると、Stop の時点ではトランスクリプトのファイルに最後のメッセージが入っているとは限りません。
出典:https://code.claude.com/docs/en/hooks 、2026-10-22確認
続けさせる返し方
| 出力 | 結果 |
|---|---|
JSON の {"decision": "block", "reason": "..."} | Claude は止まらない。reason は必須で、続ける理由として Claude に渡る |
hookSpecificOutput.additionalContext | Claude は続ける。エラーではなく「Stop hook feedback」として表示される |
exit 2 と stderr | reason と同じ扱い。stderr が説明として Claude に渡る |
exit 0 で何も出さない | Claude は止まる |
出典:https://code.claude.com/docs/en/hooks 、2026-10-22確認
テストが通るまで止まらせない hook
ダミーの todo アプリ(~/projects/todo-app)で、src/ か test/ に変更があり、npm test が失敗するときだけブロックします。
#!/usr/bin/env bash
# .claude/hooks/stop-require-tests.sh (Stop)
input=$(cat)
if [ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ]; then
exit 0 # このフックで一度続けさせたあとなので、止まらせる
fi
cd "${CLAUDE_PROJECT_DIR:?}" || exit 0
if [ -z "$(git status --porcelain -- src test)" ]; then
exit 0 # src/ と test/ に変更がない
fi
if npm test --silent >/dev/null 2>&1; then
exit 0
fi
jq -n '{decision: "block",
reason: "src/ か test/ に未コミットの変更があり、npm test が失敗しています。npm test を実行して直してから終えてください。"}'
exit 0
ダミーのリポジトリを4つの状態にして、JSON を流しました。テストは Node の組み込みのテストランナー(node --test)です。
| 状態 | stop_hook_active | 出力 | exit |
|---|---|---|---|
src/ と test/ に変更なし | false | なし | 0 |
src/ を変えてテストが落ちる | false | decision: "block" と理由 | 0 |
| 同じ状態で2回目 | true | なし | 0 |
src/ を変えてテストが通る | false | なし | 0 |
ブロックしたときの出力です。
{
"decision": "block",
"reason": "src/ か test/ に未コミットの変更があり、npm test が失敗しています。npm test を実行して直してから終えてください。"
}
exit=0
テストが通る状態での1回の実行は、0.738 秒でした。テストを回す分だけ、応答のたびに時間がかかります。ブロックのあと、Claude が本当にテストを直すかどうかは確かめていません(未実行)。
ループを防ぐ
Claude Code は、Stop hook による続行が8回続くと、次のブロックを打ち切って応答を終えます。 この回数は、Claude がツールを呼ぶたびに0に戻ります。上限は環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で上げられます。
公式のガイドは、stop_hook_active を見て、true なら早めに抜けるよう勧めています。上の例はその形で、1回止まるごとに Claude を戻すのは1回までです。テストが通るまで何度でも戻したいならこの判定を外し、上限に任せます。その場合、打ち切られるまでに最大8回ぶん、余計に使用量がかかります。
出典:https://code.claude.com/docs/en/hooks 、https://code.claude.com/docs/en/hooks-guide 、2026-10-22確認
SubagentStop との違い
SubagentStop は、サブエージェントが応答を終えたときに動きます。Stop と違って matcher があり、サブエージェントの種類(agent_type)で絞り込めます。ブロックの返し方は Stop と同じです。入力には agent_id、agent_type、サブエージェント自身のトランスクリプトのパスが加わります。プロンプトの候補を出す機能のような、Claude Code の内部のエージェントが終わったときにも動きます。
出典:https://code.claude.com/docs/en/hooks 、2026-10-22確認
settings.json と、うまく動かないとき
Stop には matcher がないので、matcher は書きません。テストを回すので、timeout を 120 秒にしています。command hook の既定は 600 秒です。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-require-tests.sh", "timeout": 120 }
]
}
]
}
}
jq で読めることと、claude doctor で不正な設定が無いことを確かめました。
注意点が2つあります。1つは、スクリプトのパスを間違えたときの扱いです。Stop hook が stdout に何も出さずに exit 2 で終わり、stderr に No such file or directory のようなファイルが無いという文が出た場合、Claude Code はそれを止めないエラーとして扱います。編集部の検証環境では、無いパスを実行すると No such file or directory で終了コード 127 でした。パスを間違えても Claude がループに閉じ込められることはありませんが、ブロックもされません。
もう1つは、onFailure: "block" が Stop hook には効かないことです。Stop で exit 2 を返すと Claude を作業に戻すことになり、動かない hook を Claude が直すことはできないからです。
出典:https://code.claude.com/docs/en/hooks 、2026-10-22確認
ほかの原因はhooksが動かないときの確認にまとめています。hook の考え方はClaude Code hooks実践にあります。
まとめ
- Stop は作業の終わりだけでなく、応答のたびに動く。重い確認は、変更が無いときに飛ばす
- 止まらせないなら
decision: "block"と必須のreasonを返すか、exit 2 と stderr で返す stop_hook_activeを見て、1回止まるごとに戻すのは1回までにする- 続行が8回続くと Claude Code が打ち切る。上限は
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられる - 最後の応答はトランスクリプトではなく
last_assistant_messageで読む - 判定は
git statusとテストの終了コードで行い、Claude の文章で判断しない