PreToolUse and UserPromptSubmit hooks silently do not fire in the VS Code extension (all matchers tested: Read/Edit/Write, Task/Agent, Bash/PowerShell)

Status Open
Reported on v2.1.259
Maintainer reply None cached
Activity 0 comments · opened Sep 4, 2026

Environment: Claude Code VS Code extension 2.1.259 (C:\Users\<user>\.vscode\extensions\anthropic.claude-code-2.1.259-win32-x64, one patch behind latest 2.1.260 at time of writing — this is not a staleness issue), Windows 10. Hooks in ~/.claude/settings.json as type: "command" running pwsh -NoProfile -File <script>.ps1.

Docs claim: "Claude Code fires the same hook events wherever it runs: sessions in the terminal, IDE extensions, the Desktop app, and Claude Code on the web." (https://code.claude.com/docs/en/hooks.md)

Observed: every registered hook tested — PreToolUse on Read|Edit|Write, Task|Agent, and Bash|PowerShell, plus UserPromptSubmit — fails to fire for a real tool call in a real session, despite each being correctly registered in settings.json and each producing correct output when invoked directly with the same JSON payload the harness would send.

Each result below is a REAL tool call in a live session (not a unit test of the hook script), deliberately isolated so that only the hook's own custom logic — nothing Claude Code could plausibly do natively — could explain a block:

  • PreToolUse on "Read|Edit|Write": a hook hard-blocks (exit 2) any Write/Edit/Read of a file named .envrc. Confirmed by direct invocation (exit 2, correct message). A REAL Write of .envrc via the Write tool succeeded, unblocked. A REAL Read of the same file also succeeded.
  • PreToolUse on "Task|Agent": a hook denies Task/Agent when a state file (written by another hook, per-session) says "tier 1". That state file was hand-seeded for the exact real session ID to force a deny decision, bypassing the (also-dead) hook that would normally write it. A REAL Agent tool call still completed normally.
  • PreToolUse on "Bash|PowerShell": a hook scans a git commit's staged diff for secret-shaped strings (e.g. a GitHub-token regex) and hard-blocks (exit 2) on a match. In a disposable scratch repo, a file matching that regex was staged (regex match confirmed independently first) and a REAL git commit was run via the Bash tool. The commit succeeded, unblocked. The identical repo/secret, fed to the hook script directly with the correct cwd in the payload, blocked correctly (exit 2, the hook's own message text).
  • UserPromptSubmit: a hook writes a per-session state file on every prompt. Across many real prompts in one long session, that file never appeared. The hook produces correct output when invoked directly with a real payload.

An earlier, weaker signal (a real Bash command mentioning .env.local was denied) initially looked like evidence Bash|PowerShell hooks fire, but the denial was the generic "Permission to use Bash with command ... has been denied" wrapper with no hook-specific text — indistinguishable from a native mechanism, an unrelated permission-mode bug, or the actual hook. The git commit secret-scan test above is the one that actually isolates the hook's own behavior, and it shows the hook not firing.

Repro (the cleanest of the four — git commit secret scan, no external side effects):

  1. Register a PreToolUse hook on matcher "Bash|PowerShell" that, when the command contains git commit, runs git -C <cwd> diff --cached and hard-blocks (exit 2) if the added lines match a secret-shaped regex (e.g. \bghp_[A-Za-z0-9]{20,}).
  2. Confirm the script works in isolation: pipe a JSON payload with the correct tool_input.command and cwd into it directly — expect exit 2 against a repo with a matching string staged.
  3. In a real VS Code extension session, in that same repo, stage a file containing a matching string and run git commit -m x via the Bash tool. Expected: blocked. Actual: commit succeeds.

Impact: PreToolUse and UserPromptSubmit hooks appear completely non-functional for real tool calls in the VS Code extension, while passing every direct-invocation test — so a hook-based safety net (secret-file blocking, destructive-git-op gating, subagent delegation caps) silently provides zero protection despite looking correctly configured, with no error surfaced anywhere. The only mechanism confirmed to actually enforce anything in this environment is the native permissions.deny allow/deny list, and only for the exact literal patterns it lists.

Related open/closed reports describing pieces of the same gap (none show a confirmed fix, and none match this exact combination of matchers + the git-commit isolation test): #15021, #20062, #21736 (closed as duplicate), #17036, #16114, #11156, #18547, #26925, #6305. One of these (#20062 lineage) shows the actual debug signature: [DEBUG] Found 0 hook matchers in settings in the VS Code extension vs. a correct non-zero count in the CLI for identical config — consistent with what's reported here.

View original on GitHub ↗