Claude Media
Claude Code hooksのonFailureで失敗時に操作を止める設定

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は既定のままにする。この使い分けが、設定の中心です。付けた直後に、わざとスクリプトを壊して止まることを一度確かめておくと、タイプミスで検査が素通りする事態を避けられます。

この記事を共有:XはてブLinkedIn