← Blogの記事一覧へ
SaaSClaude CodeHooks

Claude Code Hooks設定:exit 1・2の違いと止まらない原因

6 分で読める

このトピックを深掘りする

SaaSの導入・連携

AIに仕事の続きを渡すために、会議の原文、未完了の宿題、確認できない項目をどう残すか。導入判断と連携実装を掘り下げる。

全9本中 4 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

Claude Codeのhookが exit 1 を返したのに、ツールの処理が続く。まず、失敗した検査の終了コードがhook全体の終了コードとして返っているかを確認します。そのうえで、イベントが実行前の PreToolUse か、実行後の PostToolUse かを分けます。

2026年10月25日に公式Hooksリファレンスを確認しました。以下の表と配布モデルは、stdoutが空の同期command hookを対象にします。Claude Code本体を動かした試験ではありません。

exit 1とexit 2は、設定によって結果が変わる

PreToolUse のexit 2は、実行前のツール呼び出しを止めます。理由はstderrで渡します。exit 1や127を同じ意味で使うと、onFailure の設定によっては通常の許可フローへ戻ります。この「戻る」は実行許可を与えることではなく、その後も通常の権限判断が残ります。

終了コード PreToolUse / continue PreToolUse / block PostToolUse / continue・block
0 通常の許可フロー 通常の許可フロー 実行済み
1 通常の許可フロー 呼び出しを止める 実行済み
2 呼び出しを止める 呼び出しを止める 実行済み
127 通常の許可フロー 呼び出しを止める 実行済み

PostToolUseのexit 2では、stderrがClaudeへ渡ります。ただし、ツールの操作はすでに終わっています。検査結果を伝える用途と、実行前に止める用途を分けてください。表はツールを止められるかだけを比較しており、エラー表示の違いは再現しません。

Hooks設定ではonFailureを明示する

onFailure: "block" はClaude Code v2.1.295以降が必要です。WindowsでGit Bashがなく、shell形式のhookがonFailure未設定の場合には、失敗が呼び出しを止める例外があります。配布モデルでは未設定を受け付けず、continueかblockを明示して、この既定値の違いを除いています。失敗時の設定とWindowsの条件を使用中のバージョンと照合してください。

次は .claude/settings.json の構造を示す説明用設定です。Bashに一致する呼び出しを毎回止めるため、普段の設定へそのまま追加するものではありません。この記事の実験では設定を作成・適用しません。

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "node",
        "args": ["-e", "process.exit(2)"],
        "onFailure": "block"
      }]
    }]
  }
}

matcher、イベント名、hookの最後の終了コードを順に確認すると、「呼ばれていない」「呼ばれたが止めない」「実行後に気づいた」を切り分けられます。

後続の成功で検査の終了コードを失う

検査が2で終了しても、ラッパーがその後の成功を終了コードとして返せば、呼び出し元へ届くのは0です。次のPOSIX shell例は固定したNodeコードだけを使います。先頭の検査は必ず2、後続処理は必ず0になります。

# 終了コードを失う例。全体は0。
node -e 'process.exit(2)'
node -e 'process.exit(0)'

失敗した時点で終了すれば、後続処理へ進まず2を返せます。|| exit $? はその直前の失敗を返すPOSIX shellの書き方です。PowerShellへそのまま移す例ではありません。

node -e 'process.exit(2)' || exit $?
node -e 'process.exit(0)'

通信しないデモで16通りを比較する

判定関数 hook-exit.mjsと実行例 hook-exit-demo.mjsを同じフォルダーへ保存します。Node.jsが必要です。Bunから起動する場合も、子プロセスにはNode.jsを使います。

node hook-exit-demo.mjs
# または、BunとNode.jsがある環境で
bun hook-exit-demo.mjs

デモは固定した内部コードでexit 0・1・2・127を観測します。127も明示的に返した合成値で、実行ファイルの欠落は再現していません。2イベント×4終了コード×2設定の16通りを説明モデルへ渡し、終了コードを保つラッパーと失うラッパーも比較します。引数をコマンドとして実行せず、通信や設定ファイルの読み書き、AIの呼び出しは行いません。

出力の observedExits は [0, 1, 2, 127]、wrappers は {"preserved": 2, "swallowed": 0} になります。ラッパーの比較は、POSIX shellへ依存せずNodeの子プロセスで同じ順序を再現します。normal-permission-flow・block・already-ran は説明用の名前で、Claude Codeのログ文言ではありません。

JSONやタイムアウトへ表を広げない

有効なJSONをstdoutへ出すhookは別に確認してください。現行の公式資料では、非0の終了コードでも有効なJSONを読みます。exit 2のイベントごとの阻止効果は残り、ほかの非0ではJSONが判断を決める場合があります。ただし、失敗時にblockする設定では扱いが異なります。構造化した判断には、公式が示すexit 0と一つのJSONオブジェクトを使う方式を参照してください。

このモデルは公式仕様の解釈で、JSONの解析、timeout、起動失敗、async、ほかのイベントを検証しません。実行結果からClaude Code本体の挙動や保護の保証は導けません。次の作業へは、イベント名・明示したonFailure・最終終了コード・未確認条件を残します。コンテキスト引き継ぎの記録に添え、接続先の設定も疑わしい場合はMCP設定のスコープを確認できます。

よくある質問

Q. Claude Code Hooksでexit 1が処理を止めないのはなぜ?
空stdoutの同期PreToolUseでonFailureがcontinueなら、exit 1は通常の許可フローへ戻ります。これは実行許可ではありません。exit 2は呼び出しを止めます。block設定ではexit 1も止めます。
Q. PostToolUseのexit 2でツールを取り消せますか?
取り消せません。ツールは実行済みです。exit 2はstderrをClaudeへ渡します。実行前に止める検査はPreToolUseで扱います。
Q. onFailureを省略しても同じですか?
WindowsでGit Bashがないshell形式のhookには、未設定時に失敗が呼び出しを止める例外があります。例ではcontinueかblockを明示します。blockにはv2.1.295以降が必要です。
Q. 終了コードが非0ならJSONは無視されますか?
現行の公式資料は非0でも有効なJSONを読みます。exit 2のイベントごとの阻止効果は残り、ほかの非0は失敗時の設定にも依存します。配布モデルは空stdoutだけを扱い、JSONを検証しません。
Q. 検査はexit 2なのにhook全体が0になるのはなぜ?
ラッパーが後続の成功を返すと、検査の終了コードが失われます。POSIX shellでは検査の直後に || exit $? を置く例があります。配布デモは固定したNodeコードで保持2・消失0を比較します。