PreToolUse hooks never fire on Windows (v2.1.240)

Status Open
Reported on v2.1.240
Maintainer reply None cached
Activity 3 comments · opened Aug 22, 2026

Summary

PreToolUse hooks configured in .claude/settings.json are never called. Every tool invocation (Bash, PowerShell, Edit, Write, etc.) runs unimpeded regardless of hook configuration. SessionStart, Stop, SessionEnd and SubagentStop hooks appear unaffected — only PreToolUse is broken.

Environment

claude --version   →  2.1.240 (Claude Code)
OS                 →  Windows 11 Enterprise 10.0.26100
Shell              →  PowerShell 7+ (pwsh)

Configuration

.claude/settings.json (abbreviated):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "pwsh -NoProfile -NonInteractive -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard_autonomy.ps1\""
          }
        ]
      }
    ]
  }
}

guard_autonomy.ps1 exits with code 2 for certain commands (e.g. git branch -D, gh pr merge). Exit code 2 is documented to block the tool call.

Reproduction steps

  1. Open a Claude Code session in a project with PreToolUse hooks configured.
  2. Ask Claude to run: git branch -D __acn_hook_probe__

(This must be a Claude tool call, not prefixed with !.)

  1. Observe the output.

Expected: Hook fires → Claude sees hook output → tool call is blocked → no git output visible.
Actual: Git runs, returns error: branch '__acn_hook_probe__' not found. Hook was never invoked.

What was ruled out

| Hypothesis | Test | Result |
|---|---|---|
| Guard script logic is wrong | Fed a real PreToolUse JSON payload directly to guard_autonomy.ps1 via stdin | Script exits 2 correctly — script is not the problem |
| Path style wrong ($CLAUDE_PROJECT_DIR) | Tested: relative path, $CLAUDE_PROJECT_DIR (bash), $env:CLAUDE_PROJECT_DIR (PS), hardcoded absolute path | All four styles: hook never fired |
| Session type matters | Tested top-level session and a freshly dispatched subagent (foreground, main checkout) | Neither fires the hook |
| Matcher is too restrictive | Added an unconditional test hook (no matcher, no stdin read, exit 2 on every call) as the first entry in hooks.PreToolUse[0].hooks | echo hi ran through unblocked |
| disableAllHooks set somewhere | Searched entire machine | None found |
| CLAUDE_CODE_SIMPLE env var | Checked | Not set |
| hasTrustDialogAccepted | Checked in user settings | true |
| Settings not hot-reloaded | Changelog says hot-reload supported since v1.0.90; also tested with fresh session start | No change in either case |

The hook dispatch mechanism itself does not appear to execute hook commands for PreToolUse events, independently of configuration, path style, or session type.

Impact

This project uses PreToolUse hooks as a hard guardrail against autonomous merges, deploys, and writes to production file shares. With PreToolUse broken, these guardrails are not enforced mechanically — the automation is running on policy and in-model instructions only.

View original on GitHub ↗

3 Comments

MilkyWay008 · 7 days ago

Saw you're on Windows 11 with the PreToolUse hooks staying dead while SessionStart fires fine. One thing worth checking first: Claude Code asks to "Always allow" hook execution on first encounter, and if that prompt was ever declined the hooks silently stop firing. That's stored separately from hasTrustDialogAccepted in ~/.claude.json, so it's easy to miss. I'd also run claude --debug and grep the hook-dispatch lines to see whether the event even reaches the spawner. There were Windows hook-firing regressions before, so if dispatch shows nothing, try the latest version. Oh, btw, I built a portable Hermes OTG rescue stick for exactly these "after-update things just stop working" Windows moments, check it, imo it's handy for diagnosing a broken install. https://github.com/MilkyWay008/Hermes-OTG

hnoerenberg · 7 days ago

Follow-up: still broken in v2.1.241, root cause narrowed down

Updated to v2.1.241 and restarted — PreToolUse hooks still do not fire.

New diagnostic data from ~/.claude.json metrics

pre_tool_hook_duration_ms_count: 108
pre_tool_hook_duration_ms_avg:   0.07 ms
pre_tool_hook_duration_ms_max:   1 ms

hook_duration_ms_count:          86      ← other hook types (SessionStart etc.)
hook_duration_ms_avg:            12.3 ms ← these actually execute

The PreToolUse hooks are being dispatched (count = 108) but completing in ~0 ms — far too fast for a real pwsh process (which takes 200–500 ms to start). Other hook types (SessionStart, Stop, SessionEnd) average 12 ms, consistent with actual process execution.

Path expansion ruled out

Initial hypothesis: $CLAUDE_PROJECT_DIR not expanded → wrong file path → pwsh exits 1 → hook runner allows tool call.

Confirmed: CLAUDE_PROJECT_DIR is indeed empty in this process:

[System.Environment]::GetEnvironmentVariable("CLAUDE_PROJECT_DIR")
# → (no output)

However, path expansion is not the root cause. Tested with an inline command that needs no file path at all:

{
  "type": "command",
  "command": "pwsh -NoProfile -NonInteractive -Command \"exit 2\""
}

Result: git branch -D __acn_hook_probe__ still ran unblocked. Git returned error: branch '__acn_hook_probe__' not found. The hook did not intervene.

Conclusion

The hook command is not being executed. The dispatch layer records the event (hence count = 108 and the ~0 ms timing) but does not spawn the process. Exit code 2 never reaches the runner, so nothing is blocked.

CLAUDE_PROJECT_DIR being empty is a secondary issue worth fixing but is not what causes the blocking to fail.

Config for reference

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "pwsh -NoProfile -NonInteractive -File \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard_autonomy.ps1\""
          }
        ]
      }
    ]
  }
}

Environment

claude --version  →  2.1.241 (Claude Code)
OS                →  Windows 11 Enterprise 10.0.26100
Shell             →  PowerShell 7+ (pwsh)
hasTrustDialogAccepted: true
disableAllHooks: not set
CLAUDE_CODE_SIMPLE: not set
raazvanche · 6 days ago

Counter-datapoint that may narrow the regression window and the variable: PreToolUse hooks DO fire and DO block on CLI 2.1.237, on Windows, with a configuration that differs from the reports above in two specific ways.

Environment (working)

Claude Code CLI     2.1.237
Claude Desktop      1.34493.1.0 (MSIX)
OS                  Windows 10 Pro 10.0.19045
Hook scope          ~/.claude/settings.json   (USER level, not project)
Hook interpreter    python (14 of 14 groups)  (not pwsh)
PreToolUse groups   14, three of them fail-closed

Evidence that they execute, not just dispatch

A deny-hook on the in-app Browser pane tools, exit code 2 with the reason on stderr. Two probes:

  1. Main session - mcp__Claude_Browser__preview_start is blocked, the hook's message is returned to the model, the tool call does not run.
  2. Subagent - spawned a general-purpose subagent instructed to call the same tool once and report verbatim. It came back BLOCKED with the full hook text. So on 2.1.237 the hook layer also intercepts subagent tool calls, which is relevant to anyone relying on hooks while running parallel agents.

A synthetic probe of the same hook (piping a {"tool_name": "mcp__Claude_Browser__preview_start"} payload to it directly) returns exit 2 with the expected message, and the negative control - removing the hook file - is detected. So the hook is genuinely being spawned, not short-circuited.

Two variables that differ from the failing reports

Both failing configs above use project-level .claude/settings.json with pwsh commands. Mine is user-level ~/.claude/settings.json with python commands. Either could be the discriminator:

  • Scope is already independently suspect: #79480 reports "PreToolUse hooks in project .claude/settings.json are silently not registered", which is a very close description of dispatch-without-execution. Worth checking whether #88896 and #79480 are the same defect seen at different versions.
  • Interpreter matters because pwsh is resolved from PATH while python here is too, but the reported ~0.07 ms timing suggests no process is spawned at all, which would be interpreter-agnostic.

If someone on 2.1.240+ can test a user-level hook with a non-pwsh command, that isolates it in one step.

Regression window

Working: 2.1.237. Reported broken: 2.1.240 and 2.1.241. That puts the regression in 2.1.238 / 2.1.239 / 2.1.240.

Note on the telemetry used above

The pre_tool_hook_duration_ms_* keys are not present in ~/.claude.json on this machine at all, so absence of those metrics should not be read as absence of hook activity. A behavioural probe is more reliable than the counters for confirming enforcement.

Why this matters beyond one user

This install uses PreToolUse hooks as a safety layer: three fail-closed gates plus a tool-deny hook that exists specifically to work around the GPU crash in #81698. The failure mode described in this issue is silent - SessionStart and Stop keep working, nothing errors, and every guarded tool call simply proceeds. Anyone using hooks for compliance or safety (see also #80211) would have no signal that enforcement had stopped.

Mitigation others may want to copy: a canary that feeds one hook a synthetic payload, demands its documented verdict, and fails loudly if the verdict does not come. Run it after every CLI update, or on a schedule, and surface the result somewhere already read daily. It is about 40 lines and it turns a silent failure into a visible one.