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
PreToolUseandPermissionRequesthooks 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 Bash | shell の指定 | 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 0onFailure と 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
版を確かめる
claude --versionで、v2.1.309以降であることを見ます。 - 2
止まるケースを作る
guard.ps1がexit 2を返す条件(上の例なら再帰的なRemove-Item)をClaudeに実行させます。呼び出しがブロックされ、標準エラーに書いた理由がClaudeに返れば、exit 2は効いています。 - 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の警告の記事が別の原因を扱っています。