Claude Code hooksのonFailureで失敗時に操作を止める設定
hookが起動できない、タイムアウトする、想定外の終了コードで終わる。そんなときも操作を通してしまう既定を、onFailureのblockで止める設定を解説します。
hookの定義に "onFailure": "block" を1行足すと、そのhookが失敗したときに操作が止まります。足さなければ、スクリプトのパスを打ち間違えたhookは何も検査しないまま、すべての操作を通します。Claude Code v2.1.295で、command型とHTTP型のhookに加わった設定です。
ポリシーを守らせる目的でhookを置いているなら、既定の「失敗したら通す」は見直す価値があります。この記事では、既定の挙動、設定の書き方、失敗に数えられる条件、効かない場面、止めるhookと通すhookの選び分けを扱います。
onFailureとは何か
onFailure は、hookそのものが失敗したときに、そのhookが関わる操作をどうするかを決める設定です。値は "continue"(既定)か "block" の2つで、v2.1.295以降で使えます。対象はcommand型とHTTP型で、mcp_tool 型、prompt 型、agent 型のフィールド表には載っていません。
"block" を指定すると、失敗は「そのイベントで終了コード2を返した場合」と同じ扱いになります。PreToolUse ならツール呼び出しが止まり、UserPromptSubmit ならプロンプトがClaudeに届きません。この追加が入ったリリース全体の内容はv2.1.295の解説にまとめています。
既定では、壊れたhookは操作を通す
終了コードの扱いを整理すると、既定で「通す」側に倒れる場面が見えてきます。
| hookの状態 | 既定の結果 |
|---|---|
| 終了コード0 | 既定の結果成功。JSON出力があればその内容が適用される |
| 終了コード2 | 既定の結果ブロック(ブロックできるイベントの場合) |
| 0と2以外の終了コード | 既定の結果非ブロッキングエラー。操作は続く |
| スクリプトが存在しない・実行できない | 既定の結果同じく非ブロッキングエラー。操作は続く |
| タイムアウト | 既定の結果hookは取り消され、PreToolUse ではツール呼び出しが続く |
ドキュメントも、タイムアウトしたhookをゲートとして当てにしないよう明記しています。一般的なUnixの感覚では終了コード1は失敗ですが、JSONを出力しない限り、1は非ブロッキングエラーに留まります。
困るのは、ポリシー用のhookです。settings.json のパスを1文字間違えると、シェルが127のような終了コードで落ちます。画面には hook error の通知が出るだけで、ツール呼び出しは通ります。通知を見逃した状態が続くと、検査は一度も走りません。
設定の書き方
onFailure は、hookハンドラのフィールドとして command や url と同じ階層に置きます。次は PreToolUse でBashコマンドを検査し、検査スクリプトが動かなかったら止める例です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
"onFailure": "block"
}
]
}
]
}
}この形はhooksリファレンスの例そのものです。args を付けると、シェルを介さずに実行ファイルを直接起動します。
HTTP型でも同じ名前のフィールドです。接続できない、またはレスポンスが2xxでないときに止めたい場合は、次のように書きます。
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"onFailure": "block"
}HTTP型は、ステータスコードだけではブロックを伝えられません。非2xxや接続失敗は、既定では非ブロッキングエラーです。ブロックしたいときはJSONの決定フィールドを2xxで返すか、この onFailure を使います。
失敗に数えられる5つの場合
"block" が発動する条件は、ドキュメントに5つ挙がっています。
onFailureが失敗と見なす条件
起動できない
command型のスクリプトや実行ファイルが存在しない場合などです。
0と2以外の終了コード
command型が、許可を示すJSONを出力していても失敗に数えられます。
HTTPエラー
接続に失敗した、またはレスポンスのステータスが2xxでない場合です。
タイムアウト
hookの
timeoutに達した場合です。出力が不正
JSONとして解析できない、またはスキーマ検証に失敗した場合です。
ここで見落としやすいのが2つ目です。exit 1 で終わりながら permissionDecision: "allow" を含むJSONを出力したhookは、onFailure なしなら「JSONの内容が決める」成功扱いでした。"block" を付けると、この組み合わせは失敗になります。JSONで判断を返したいときは、exit 0 で終えます。
HTTP型で、2xxなのにボディがプレーンテキストの場合も失敗です。一方、command型のプレーンテキストの標準出力は、失敗には数えられません。
起動失敗の見え方は、execとshellで変わる
command型には、args を付ける実行ファイル直接起動(exec形式)と、付けないシェル経由(shell形式)があります。onFailure: "block" は、どちらでも「起動できない」を失敗として扱います。違うのは、失敗したときに出るメッセージです。
shell形式でスクリプトのパスが存在しないと、シェルが127のような終了コードで終わります。通知には /bin/sh: /path/to/hook.sh: No such file or directory のようなシェルのメッセージが載ります。exec形式では、node が Cannot find module と返すなど、起動した実行ファイル自身のエラーが付きます。リファレンスの動作確認例は、後者の形です。
exec形式には、書き間違いを呼ぶ落とし穴が1つあります。command に実行ファイル名だけを書き、node script.js のように空白を含めると、そんな名前の実行ファイルは存在せず、起動に失敗します。"command": "node" と "args": ["script.js"] に分けて書きます。"block" を付けていれば、この書き間違いは「操作が止まる」ことで早く気づけます。付けていないと、通知が出るだけで操作は通ります。
timeoutの値が、止まるまでの待ち時間を決める
タイムアウトも失敗に数えられるので、timeout の値は、操作が止まるまでの待ち時間そのものになります。command型、HTTP型、mcp_tool 型の既定は600秒です。UserPromptSubmit では30秒に下がります。何も指定しないまま PreToolUse のHTTP hookを "block" にすると、エンドポイントが応答しない間、ツール呼び出しは最長で10分待たされる計算です。
ゲート役のhookには、onFailure と一緒に、現実的な timeout も書いておきます。先ほどのHTTP型の例で "timeout": 30 と書いたのは、そのためです。
止まり方はイベントごとの終了コード2に従う
失敗時の動作は、そのイベントで終了コード2を返したときの動作と同じです。たとえば PreToolUse はツール呼び出しの中止、UserPromptSubmit はプロンプトの拒否になります。
例外は PermissionRequest です。このイベントは終了コード2を無視しますが、onFailure: "block" の失敗は「リクエストを拒否する」動作になります。
逆に、終了コード2でブロックできないイベント(PostToolUse や Notification など)で "block" を付けても、すでに起きた操作を取り消せるわけではありません。どのイベントが止められるかは、リファレンスの「Exit code 2 behavior per event」の表で確認できます。
効かないhook
次のhookでは、onFailure は何もしません。
Stop、SubagentStop、TaskCompleted、TeammateIdle: これらのイベントで終了コード2を返すと、Claudeが作業に戻されます。壊れたhookが原因でClaudeを作業に戻し続けても、Claudeはhookを直せないためです- バックグラウンドのcommand hook:
asyncやasyncRewakeを指定したものです。実行を待たないので、止める対象がありません
あわせて PreModelSwitch は、既定でタイムアウトしたhookがモデル切り替えをブロックします。このイベントでは、タイムアウトに関しては追加の設定は要りません。
動作確認:わざと壊して止まるかを見る
設定は、書いたら壊して試すのが確実です。リファレンスの手順は、検査スクリプトを置かないままClaudeに ls を実行させるものです。止まれば、エラーに failed; blocking because onFailure is "block" が含まれます。タイムアウトの場合は failed が timed out に変わります。
実プロジェクトでは、Claudeにこの確認を頼むこともできます。例えば次のように伝えます。
.claude/hooks/check-command.js を一時的にcheck-command.js.bakへ改名してから、
ls を実行して。結果のエラーメッセージをそのまま見せて、確認後に名前を戻してエラーに blocking because onFailure is "block" が出れば、設定は効いています。出ずに ls の結果が返ってきたら、onFailure の位置(ハンドラの階層)か、matcher を確認します。ドキュメントによると、onFailure なしで同じスクリプトが欠けた場合は、非ブロッキングエラーで ls が実行されます。
止めるhookと通すhookの選び分け
"block" は失敗を安全側に倒しますが、副作用もあります。HTTPのエンドポイントが一時的に落ちれば、対象の操作はすべて止まります。hookごとに役割で決めるのが現実的です。
onFailureをhookの役割で決める
blockを付ける
rm や本番設定の書き換えを止める検査、機密ファイルへのアクセス制御、組織のポリシーを強制するhookです。検査が動かないことが、そのまま事故につながります。
既定のcontinueのまま
通知、ログの送信、Lintの自動修正のようなhookです。落ちても作業を続けたいので、止めると逆に作業の邪魔になります。
PostToolUseのLint自動修正やテストの自動実行のように、操作の後に走るhookは、そもそもブロックの対象外です。onFailure が意味を持つのは、主に PreToolUse と UserPromptSubmit のような、操作の前に挟まるhookだと言えます。
hookをmarkdown1枚で作る方法を知りたい場合は、hookifyの解説が参考になります。ただし、生成されるのはhookifyの規則であって、ここで扱った onFailure を設定する対象とは別物です。
スクリプト側でできる備え
onFailure: "block" はスクリプトの外側からの安全網です。内側にも、ポリシーを守らせる際の基本があります。
- 止めたいときは
exit 2を返す(終了コード1はJSONなしでは非ブロッキングのまま) - JSONで判断を返す場合は
exit 0で終える jqなど外部コマンドに依存するなら、スクリプト冒頭で存在を確かめる。onFailure: "block"を付けた状態でjqが無ければ、検査は走らず操作が止まる
次はBashの rm を止める、リファレンス掲載の最小スクリプトです。
#!/bin/bash
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed" >&2
exit 2
fi
exit 0このスクリプトを "onFailure": "block" のhookから呼ぶと、rm で始まるコマンドは終了コード2で止まります。スクリプト自体が動かない場合も、同じように止まります。
まとめ
ゲート役のhookには "onFailure": "block" を付け、補助役のhookは既定のままにする。この使い分けが、設定の中心です。付けた直後に、わざとスクリプトを壊して止まることを一度確かめておくと、タイプミスで検査が素通りする事態を避けられます。