PreToolUse hooks stop firing after EnterWorktree switches session into a linked worktree (Windows, v2.1.207); /hooks reload doesn't fix it

Open 💬 0 comments Opened Jul 12, 2026 by crisradu75

Environment

  • Claude Code 2.1.207
  • Windows 11
  • Project-level .claude/settings.json (committed, not .claude/settings.local.json)

Summary

PreToolUse hooks configured in a project's .claude/settings.json stop firing on live tool calls after the session calls EnterWorktree (e.g. via /new-worktree) to switch into a linked git worktree — even though the linked worktree has its own checked-out, byte-identical copy of .claude/settings.json, and even though the hook scripts themselves are correct and work fine when invoked directly. Running /hooks to reload did not restore them. A full session restart was the next untried step at the time of writing.

Repro steps

  1. In a repo with PreToolUse hooks wired for Bash and Edit|Write matchers in .claude/settings.json (e.g. a hook that blocks any Bash command containing cd , exiting 2).
  2. Mid-session, call EnterWorktree (directly or via a skill/command like /new-worktree) to create and switch into a new linked worktree.
  3. As a live tool call, run a Bash command that the hook should block — e.g. cd /tmp && pwd.
  4. Observed: the command runs to completion (exit 0), unblocked.
  5. Sanity-check the hook script is correct: pipe the exact same JSON payload the tool call would have sent directly into the hook: echo '{"tool_name":"Bash","tool_input":{"command":"cd /tmp && ls"}}' | python .claude/hooks/<hook>.py. Observed: exits 2 with the expected block message — the hook logic itself is fine.
  6. Run /hooks to reload configuration, then repeat step 3. Observed: still not blocked.

Expected

The live tool call is blocked exactly as the direct pipe invocation is, since the hook config and script are unchanged and correct.

Actual

The hook is silently skipped for the live tool call. No error, warning, or any signal to the user or the model that hooks are inactive — the session behaves exactly as if no hooks were configured at all.

Additional checks done

  • .claude/settings.json was validated as well-formed JSON and checked against the documented hooks schema (type/command/matcher shape) — this doesn't look like the "one schema-invalid hook matcher disables all hooks" failure mode (#75081); the config was valid both before and after this was observed.
  • This wasn't isolated to one specific hook script — multiple different PreToolUse hooks (a Bash-matcher blocker and an Edit|Write-matcher blocker) both failed to fire in the same session, while both worked correctly when piped directly.
  • I did not have a chance to confirm hooks were firing correctly in this same session before EnterWorktree ran (only tested after), so I can't rule out an unrelated pre-existing cause — but the timing (failure discovered immediately after a worktree switch, with two different hooks affected, in a worktree whose settings.json is byte-identical to the one that presumably worked pre-switch) points at the worktree/cwd transition as the likely trigger.

Impact

Any safety-critical PreToolUse hook (isolation guards, push-to-main blockers, path-escape guards, etc.) becomes fully inert for the rest of the session once this triggers, with zero visible indication — a user or agent has no way to know the guardrails they're relying on have gone silently dark.

Possibly related

  • #64699 — Hooks stop firing entirely after editing settings.local.json (Windows)
  • #76413 — PreToolUse hooks intermittently not invoked at all (VSCode extension host, Windows)
  • #74942 — PreToolUse hooks with Edit|Write matcher silently never invoked for an entire session (macOS)

None of these mention a worktree switch as the trigger, but they may share a root cause (some session-state transition silently detaching the live hook engine from a still-valid config). Happy to provide the full (non-sensitive) .claude/settings.json hook wiring and hook scripts if useful for repro.

View original on GitHub ↗