Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
Summary: I have a project-level .cursor/hooks.json with 8 hooks for a build enforcement system. The preToolUse hook was confirmed working at the start of a session (it blocked file writes and returned {permission: “deny”}). During the session, I added a second preToolUse entry with a different matcher. After that modification — and across 4 Cursor restarts — no hooks fire at all, including the originally working preToolUse.
Timeline:
Session started with hooks.json containing one preToolUse entry (matcher: “Write|StrReplace|Delete”). This hook worked — it blocked writes to implementation files and returned {permission: “deny”} with messages visible to the agent.
During the session, I added a second preToolUse entry (matcher: “Shell|CallMcpTool”) for evidence logging. After this change, neither preToolUse entry fired.
I tried multiple approaches:
Reverted hooks.json to original (single preToolUse entry) — no hooks fire
Wrapped all commands in powershell -NoProfile -ExecutionPolicy Bypass -Command “node script.js” — no hooks fire
Created a .ps1 wrapper script and used powershell -File .cursor/hooks/wrapper.ps1 — this worked once (received full JSON payload with conversation_id, model, tool_name, tool_input) but stopped working after the next hooks.json edit
Restarted Cursor 4 times — no recovery
Current state: hooks.json is restored to original format. No hooks fire. Writing to a file that should be blocked by preToolUse (with failClosed: true) goes through without intervention.
How I confirmed hooks aren’t firing:
Added fs.appendFileSync at module level (line 3, before any function) in hook scripts — file never created by Cursor, but IS created when script is run manually via echo ‘{}’ | node .cursor/hooks/post-tool-nudge.js
Wrote to src/lib/fund/test.ts which the preToolUse gate should block (no active gate file exists, failClosed: true). Write was allowed through.
All scripts pass node -c syntax check
All scripts run correctly when invoked manually and produce valid JSON output
The .cursor/hooks/state/ directory exists and is writable
Current hooks.json:
{
“version”: 1,
“hooks”: {
“sessionStart”: [
{ “command”: “node .cursor/hooks/session-init.js”, “timeout”: 5 }
],
“preToolUse”: [
{
“command”: “node .cursor/hooks/pre-dev-gate.js”,
“matcher”: “Write|StrReplace|Delete”,
“failClosed”: true,
“timeout”: 5
}
],
“postToolUse”: [
{ “command”: “node .cursor/hooks/post-tool-nudge.js”, “timeout”: 3 }
],
“preCompact”: [
{ “command”: “node .cursor/hooks/pre-compact.js”, “timeout”: 5 }
],
“beforeShellExecution”: [
{ “command”: “node .cursor/hooks/shell-gate.js”, “timeout”: 5 }
],
“afterShellExecution”: [
{ “command”: “node .cursor/hooks/evidence-logger.js”, “timeout”: 3 }
],
“afterMCPExecution”: [
{ “command”: “node .cursor/hooks/browser-evidence.js”, “timeout”: 3 }
],
“stop”: [
{ “command”: “node .cursor/hooks/completion-checker.js”, “loop_limit”: 3, “timeout”: 10 }
]
}
}
What I found during debugging:
The one time a hook DID work after the initial breakage was using a native .ps1 script (not a Node wrapper):
{
“command”: “powershell -NoProfile -ExecutionPolicy Bypass -File .cursor/hooks/test-hook.ps1”,
“matcher”: “Shell”,
“timeout”: 3
}
The .ps1 script used [Console]::In.ReadToEnd() to read stdin, logged the payload, and returned {“permission”:“allow”}. The log showed the full Cursor payload:
input=n++{“conversation_id”:“a0cae581-…”,“model”:“claude-4.6-opus-max”,“tool_name”:“Shell”,“tool_input”:{“command”:"cd c:\Us…
Note the n++ prefix before the JSON — this may be relevant. After editing hooks.json again to apply this pattern to other hooks, even this approach stopped working.
File structure:
.cursor/
├── hooks.json
└── hooks/
├── session-init.js
├── pre-dev-gate.js
├── post-tool-nudge.js
├── pre-compact.js
├── shell-gate.js
├── evidence-logger.js
├── browser-evidence.js
├── completion-checker.js
├── shared-checks.js
├── run-node-hook.ps1
├── pre-tool-evidence.js
└── state/
└── .gitkeep
Questions:
- Can modifying hooks.json during a session put the hooks system into a permanently broken state that persists across restarts?
- What does the n++ prefix in the stdin payload mean? Is that expected, and could it be breaking JSON parsing in Node scripts?
- Where can I see hook loading errors? I haven’t checked Help > Toggle Developer Tools > Console or Settings > Hooks tab or View > Output > Cursor > Hooks yet — are those the right places to look?
- Is there a known issue with node commands in hooks on Windows? The docs show shell scripts (./hooks/script.sh) in examples — is Node invocation via node .cursor/hooks/script.js officially supported on Windows?
- Does adding/removing entries in hooks.json require anything beyond a file save for Cursor to reload? The docs say Cursor watches hooks.json and reloads automatically.
Steps to Reproduce
See Description
Expected Behavior
Hooks should fire when expected
Operating System
Windows 10/11
Version Information
Version: 2.6.18 (system setup)
VSCode Version: 1.105.1
Commit: 68fbec5aed9da587d1c6a64172792f505bafa250
Date: 2026-03-10T02:01:17.430Z
Build Type: Stable
Release Track: Default
Electron: 39.6.0
Chromium: 142.0.7444.265
Node.js: 22.22.0
V8: 14.2.231.22-electron.0
OS: Windows_NT x64 10.0.26200
For AI issues: which model did you use?
Opus 4.6 Max
Does this stop you from using Cursor
No - Cursor works, but with this issue