[Docs] Windows PowerShell hooks: exit code from .ps1 scripts not propagated to Claude Code

Resolved 💬 2 comments Opened Feb 10, 2026 by ssv555 Closed Mar 11, 2026

Documentation Type

Missing documentation (feature not documented)

Documentation Location

https://code.claude.com/docs/en/hooks

Section/Topic

Exit code output / Hook handler fields

Current Documentation

The docs provide only Bash/shell examples for hooks. The "Exit code output" section shows a bash script example but no PowerShell equivalent. The docs say:

"Exit 2 means a blocking error. stderr text is fed back to Claude as an error message."

But there is no mention of how to achieve this on Windows with PowerShell scripts.

What's Wrong or Missing?

On Windows, when calling a .ps1 script from settings.json using powershell -Command "& script.ps1", the script's exit 2 does NOT propagate to the PowerShell process exit code. The process always exits 0, so Claude Code never sees the blocking signal.

Both & $f (call operator) and . $f (dot-source) fail to propagate exit codes.

The only working pattern is: & script.ps1; exit $LASTEXITCODE

This is undocumented and took hours of debugging to discover. Windows users attempting to write PreToolUse hooks that block commands will silently fail with no indication of what's wrong.

Reproduction:

  1. Create test-hook.ps1: [Console]::Error.WriteLine("BLOCKED"); exit 2
  2. Register in settings.json with: powershell -Command "& test-hook.ps1"
  3. Hook fires but exit code 2 is lost — commands are NOT blocked
  4. Change to: powershell -Command "& test-hook.ps1; exit $LASTEXITCODE"
  5. Now commands ARE blocked correctly

Suggested Improvement

Add a "Windows / PowerShell" section or tab to the hooks reference with:

  1. The required ; exit $LASTEXITCODE pattern:
{
  "command": "powershell -ExecutionPolicy Bypass -Command \"& (Join-Path $env:USERPROFILE 'hooks/block.ps1'); exit $LASTEXITCODE\""
}

### Impact

High - Prevents users from using a feature

### Additional Context

Add a "Windows / PowerShell" section to the hooks reference covering the exit code propagation gotcha.

When calling a .ps1 script via "powershell -Command", the script's exit code is silently lost. The required pattern is to append "; exit $LASTEXITCODE" after the script call:

    "command": "powershell -ExecutionPolicy Bypass -Command \"& (Join-Path $env:USERPROFILE 'hooks/block.ps1'); exit $LASTEXITCODE\""

Include a minimal PowerShell hook example equivalent to the existing bash block-rm.sh:

    $data = [Console]::In.ReadToEnd() | ConvertFrom-Json
    $command = $data.tool_input.command
    if ($command -match 'rm\s+-rf') {
        [Console]::Error.WriteLine("Destructive command blocked")
        exit 2
    }
    exit 0

Also note that $env:USERPROFILE works in hook commands because hooks run via the system shell (cmd.exe on Windows), not Git Bash, so dollar signs are not consumed.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗