Claude Media
PowerShell hookがexit 2で止まらない問題 — Claude Codeの対処

PowerShell hookがexit 2で止まらない問題 — Claude Codeの対処

PowerShellで動くhookがexit 2を返してもツール呼び出しが止まらない問題は、v2.1.309で修正されました。古い版の見分け方と、shellとonFailureの設定の組み合わせを解説します。

PowerShellで動かしているhookが exit 2 を返しても、ツール呼び出しが止まらない。Windowsでガード用のhookを組んだ人が踏む不具合で、Claude Code v2.1.309で修正されました。まず claude --version を実行し、v2.1.308以前なら更新するのが最初の一手です。

ただし更新だけでは足りない構成があります。Git Bashの有無で、hookを動かすシェルと失敗時の扱いが変わるためです。この記事では、修正の中身、shell と onFailure の組み合わせ、Git Bashを入れた後に効かなくなる落とし穴を扱います。

修正されたのは、exit 2とfail openの2点

変更履歴には、v2.1.309の項目として次の1行が載っています。

Fixed hooks that run in PowerShell not blocking when the hook's program exits with code 2, and on Windows PreToolUse and PermissionRequest hooks with "shell": "powershell" failing open when Git for Windows isn't installed

1行の中に、別々の2つの不具合が入っています。

修正内容

v2.1.309で直った2つの症状

  • exit 2でブロックされない

    PowerShellで動くhookのプログラムが終了コード2で終わっても、ブロックとして扱われませんでした。

  • Git Bashなしでfail open

    Git for Windowsが無い環境で "shell": "powershell" を指定した PreToolUse と PermissionRequest のhookが、失敗時に操作を通していました。

fail openは、検査が壊れたときに「通す」側へ倒れる挙動です。ポリシーを守らせるhookにとって、最も避けたい倒れ方です。

直後のv2.1.310には、関連する別の修正があります。Git Bashが無く、hookの command が引用符付きの文字列だけ(引用符で囲んだパスなど)のとき、PreToolUse と PermissionRequest のhookが呼び出しを通していた問題です。これは後半の「PowerShell started no program」の節で扱います。

まず自分の環境がどの組み合わせか確かめる

hookがどのシェルで動くかは、shell フィールドとGit Bashの有無で決まります。hooksリファレンスの記述を表にまとめます。

Git Bashshell の指定command型のシェル形式で使われるシェル
ありshell の指定未指定command型のシェル形式で使われるシェルbash
なしshell の指定未指定command型のシェル形式で使われるシェルPowerShell
どちらでもshell の指定"powershell"command型のシェル形式で使われるシェルPowerShell
どちらでもshell の指定"bash"command型のシェル形式で使われるシェルbash

PowerShellが選ばれたときは、PowerShell 7以降の pwsh.exe を自動検出し、見つからなければWindows PowerShell 5.1の powershell.exe に切り替わります。shell に "powershell" を指定するだけで動き、PowerShellツール用の CLAUDE_CODE_USE_POWERSHELL_TOOL は要りません。hookが直接PowerShellを起動するためです。

なお args を付けた実行ファイル直接起動(exec形式)では、シェルが介在しないので shell は無視されます。

PowerShellツールそのものの有効条件は、Claude CodeのBashツールとシェル起動設定にまとめています。hookの shell とは別の設定です。

Git Bashが無い環境では、終了コード0と2以外が失敗になる

Git Bashが無いWindowsで、shell を指定しないシェル形式の PreToolUse または PermissionRequest のcommand hookが0と2以外の終了コードで終わると、呼び出しがブロックされます。通常のhookなら「非ブロッキングエラー」で操作が続く場面です。

このとき、エラーには次の文言が含まれます。

blocking because Git Bash wasn't found, so only exit code 0 or 2 counts as this hook's verdict.

v2.1.309より前は、文言が blocking because Git Bash wasn't found and this hook doesn't set "shell" to "powershell", so only exit code 0 or 2 counts as its verdict. でした。古い版の記事やissueで後者を見たら、同じ状況の話です。

この環境では、終了コード0と2だけがhookの判定として数えられ、それ以外は失敗扱いになります。exit 1 で警告を出すつもりのスクリプトが、ここで止まる形で表に出ます。

原因を調べるときは、デバッグログを使います。

claude --debug-file ./claude-debug.log

ログには、hookの終了を表す Hooks: で始まる行が残ります。v2.1.296以降は、イベント名・hookのコマンド・終了状態・所要時間が1行に並びます。

対処は、hookの目的で選ぶ

リファレンスは、Git Bashが無い環境での失敗を5つの対処に振り分けています。自分のhookがどれに当たるかで選びます。

hookの状況対処
失敗しても呼び出しは通したい対処"onFailure": "continue" を足す
exit 1 で警告し、クラッシュ時はブロックしたい対処"onFailure": "block" を足し、警告したい分岐は exit 0 に変える
Bash向けに書いたスクリプトを動かしたい対処Git for Windowsを入れる。入れた場所が検出されなければ CLAUDE_CODE_GIT_BASH_PATH を設定し、新しいセッションを始める
.exe 1つを動かす対処command に実行ファイル、args に引数を置くexec形式にし、"onFailure": "block" を足す
PowerShellのガードhookで、失敗時は必ず止めたい対処"onFailure": "block" と "shell": "powershell" の両方を足す

警告を出しつつ通したい場合は、PreToolUse なら標準出力に systemMessage を含むJSONを1つ出し、exit 0 で終えます。

ガードhookには、onFailureとshellを両方書く

PowerShellでガードを書くなら、最終形は次のとおりです。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell",
        "hooks": [
          {
            "type": "command",
            "shell": "powershell",
            "command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\guard.ps1\"",
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

matcher を Bash|PowerShell にしているのは、シェルコマンドを検査するhookを両方のツールに掛けるためです。コマンドの書き方は、PowerShell用のリファレンス例にある & "$env:CLAUDE_PROJECT_DIR\.claude\hooks\check.ps1" の形に揃えています。${CLAUDE_PROJECT_DIR} も使えますが、$CLAUDE_PROJECT_DIR と素のまま書くとPowerShellが未定義のローカル変数として $null に解決し、スクリプトのパスからプロジェクトのルートが抜けます。

guard.ps1 の側は、止めたいときに標準エラーへ理由を書いて exit 2 します。許可するときは何も出さずに exit 0 します。次は書き方の一例です。

# .claude/hooks/guard.ps1
$callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
$command = $callInput.tool_input.command
 
if ($command -match 'Remove-Item.*-Recurse') {
  [Console]::Error.WriteLine('Recursive delete is blocked by policy')
  exit 2
}
exit 0

onFailure と shell を両方書く理由は、組み合わせごとの挙動の違いにあります。

  • onFailure だけ: Git Bashの有無にかかわらず、失敗とタイムアウトでブロックされます。ただし、Git Bashを入れるとhookがbashで動き始め、PowerShell用に書いたスクリプトが動かなくなります
  • shell だけ: 常にPowerShellで動きますが、失敗時の扱いは onFailure が未設定のままなので、Git Bashがある環境では失敗しても操作が通ります

リファレンスは「onFailure が未設定のPowerShellガードhookは、Git for Windowsを入れてClaude CodeがGit Bashを見つけると、失敗時にブロックしなくなる」と説明しています。チームの一部だけがGit Bashを入れている場合、同じ settings.json でも人によって挙動が分かれうるのはこのためです。

onFailure 自体の仕様と、失敗に数えられる条件はhooksのonFailureで失敗時に操作を止める設定にあります。追加された版の全体像はv2.1.295のリリースノートで読めます。

直ったかどうかを手元で試す

修正後の版でガードが効くかは、わざと失敗させて確かめられます。手順は3つです。

手順

ガードhookの動作確認

  1. 1

    版を確かめる

    claude --version で、v2.1.309以降であることを見ます。

  2. 2

    止まるケースを作る

    guard.ps1 が exit 2 を返す条件(上の例なら再帰的な Remove-Item)をClaudeに実行させます。呼び出しがブロックされ、標準エラーに書いた理由がClaudeに返れば、exit 2 は効いています。

  3. 3

    壊れたケースを作る

    guard.ps1 を別名に変えて、無害なコマンド(ls など)を実行させます。onFailure: "block" が効いていれば、スクリプトが無くても呼び出しは止まります。通ってしまうなら、onFailure か shell の書き忘れを疑います。

テストが終わったら guard.ps1 の名前を戻します。

exit 2が効かないイベントを確認する

修正後でも、exit 2 の意味はイベントごとに違います。

  • PreToolUse: ツール呼び出しをブロックする
  • UserPromptSubmit: プロンプトをブロックする
  • PostToolUse: ツールは実行済みなので、標準エラーがClaudeに表示されるだけ
  • PermissionRequest: 終了コード2は使われず、権限の流れはそのまま進む

PermissionRequest で拒否したいときは、decision オブジェクトで返します。onFailure: "block" を付けたhookの失敗だけは、このイベントでもリクエストの拒否として扱われます。

PowerShell started no programが出たとき

エラーに PowerShell started no program が含まれる場合は、command が引用符付きの文字列だけになっています。たとえば "command": "\"C:\\Tools\\guard.exe\"" のような形です。PowerShellはこれを実行せず、文字列として出力するだけで、プログラムは何も起動しません。

直し方は、目的で分かれます。

  • 文字列(JSONなど)を出力したい: "shell": "powershell" を指定します。hookは exit 0 で終わります
  • .ps1・.cmd・.bat を動かす、または変数を含むパスの .exe を動かす: "shell": "powershell" を指定し、引用符付きのパスの前に & を付けます。$HOME はそのまま書き、環境変数は $env:CLAUDE_PROJECT_DIR の形にします
  • $ を含まないパスの .exe を1つ動かす: command の内側の引用符を外し、exec形式にします。引数が無いなら "args": [] と書きます

落とし穴

`.exe` のパスを引用符で囲んだだけだと、何も実行されない

"command": "\"C:\\Tools\\guard.exe\"" は、PowerShellにとって「文字列を出力せよ」という命令です。onFailure が未設定でGit Bashもあれば、hookが何もしないまま呼び出しが通ります。v2.1.310の修正はGit Bashが無い環境の穴を塞ぎましたが、書き方そのものは直してくれません。& を付けるか、exec形式に変えます。

環境変数の持ち越しはPowerShell hookで使えない

SessionStart などが使う CLAUDE_ENV_FILE は、"shell": "powershell" を指定したhookでも、Git Bashが無くて既定でPowerShellになったhookでも使えません。環境変数を渡す用途のhookは、bashで動かす構成にします。なお、PowerShellツールで実行するコマンドの側は、ファイルの中身が単純な代入だけなら変数を受け取れます。この区別はシェル起動設定の記事で扱っています。

まとめ

PowerShell hookの exit 2 が止まらない不具合は、v2.1.309で直りました。版を確認したうえで、ガード用のhookには "shell": "powershell" と "onFailure": "block" を両方書くと、Git Bashの有無で挙動が変わりません。警告だけで通したい分岐は exit 1 を避け、exit 0 と systemMessage で書きます。

PowerShell自体の起動エラーはPowerShell(x86)エラーの切り分け、Bashを拒否するとPowerShellツールも止まる警告はDenying Bashの警告の記事が別の原因を扱っています。

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