Why your Cursor hook doesn't fire, and what it can't inject
A hook is a script Cursor runs at a fixed point in a session: session start, before a shell command, after a file edit, when a subagent finishes. Cursor sends JSON on stdin and reads JSON or an exit code back. It is how you enforce a boundary the model cannot be trusted with, or record something it should not have to remember. A subagent is a separate agent Cursor launches for a task, with its own context window; MCP is the protocol Cursor uses to reach external tool servers.
The Cursor-specific failure is a hook that runs, blocks the action as intended, and still never gets its text to the model: additional_context is the documented way to add context, it exists on only a few events, and it has been unreliable on them (the chronology is below). From the chat, four failures look alike: it never loaded (no line in Output → Hooks), it never matched (other hooks run, yours never logs), it failed open (it logs and the command still runs), or its output was dropped (it logs and blocks but the agent never sees the text). The first three are the usual causes; the last is the one Cursor makes easy to miss.
Run Cursor → About Cursor in the editor, and agent --version in the CLI. Plugin hooks are the build-sensitive part: the editor failed to register them on 3.0.12 and 3.2.21, and the CLI changelog for v2026.08.11 says installed plugin hooks now execute. Cursor 3.27 was the newest editor build on 27 October 2026; the CLI changelog listed v2026.10.22.
Checked against Cursor's documentation, changelog and forum reports on 27 October 2026.
Status, 27 October 2026: plugin hooks can fail to register with no visible error, and additional_context has been accepted and logged without reaching the model in several 2026 builds. Both look like a hook that never fired.
Where a hook has to live#
Cursor reads four sources, then plugin hooks. All matching hooks run. Permission responses merge deny over ask over allow; other fields resolve by priority, Enterprise → Team → Project → User, with plugin hooks last, so a lower-priority source can override a higher one.
| Source | File | Runs from | Notes |
|---|---|---|---|
| Project | <repo>/.cursor/hooks.json | The project root | Only in a trusted workspace; commit it to share |
| User | ~/.cursor/hooks.json | ~/.cursor | Every project on that machine; cloud agents cannot read it |
| Enterprise | macOS /Library/Application Support/Cursor/hooks.json; Linux/WSL /etc/cursor/hooks.json; Windows C:\ProgramData\Cursor\hooks.json | The enterprise config directory | MDM-managed, system-wide |
| Team | Cursor dashboard | The managed hooks directory | Enterprise plans; syncs about every 30 minutes |
| Plugin | hooks/hooks.json inside the plugin | The plugin root (CURSOR_PLUGIN_ROOT), except stop and subagentStop, which run from the workspace root | Command hooks only, not prompt hooks |
The working directory is part of the config. Project hooks run from the project root, so command is .cursor/hooks/check.sh; user hooks run from ~/.cursor, so the same script is ./hooks/check.sh. A path copied between them looks for a different file, and the chat looks the same. Use an absolute path if unsure.
The Loaded N user hook(s) line is not a complete inventory for plugins. A staff reply on 12 July 2026 says it does not count plugin hooks; use Output → Hooks execution lines and the script's log (thread).
Pick the event by what it can do#
Native Cursor hooks use camelCase. PascalCase names (PreToolUse) belong to Claude Code files and are mapped only when Cursor loads .claude/settings*.json; in a native file, Cursor can reject one as an unknown hook type.
| What you want | Event | What it can do |
|---|---|---|
| Gate one shell command | beforeShellExecution | Allow, ask, or deny |
| Gate any tool call | preToolUse | Allow or deny; ask is accepted by the schema but not enforced, so use beforeShellExecution when you need a prompt. A Shell matcher limits it to shell commands |
| Gate a file read | beforeReadFile | Allow or deny |
| Format or audit an edit | afterFileEdit | Observe only |
| Add context when a session starts | sessionStart | Fire-and-forget; additional_context, but see the delivery gap below |
| Keep the agent working at the end of a turn | stop | followup_message auto-submits a new user message |
| Clean up when a session ends | sessionEnd | Observe only; described as composer, tab, or window close, not turn end; see the gap below |
| Notice compaction | preCompact | Observe only; cannot block or change the summary |
A matcher is a regex over the event's subject: the tool type for preToolUse (Shell, Read, Write, Grep, Delete, Task, MCP:<tool_name>), the subagent type, the command text for shell hooks, or a fixed label. Empty or * matches everything. matcher: "Bash" never matches — Cursor calls the tool Shell; for MCP tools use MCP:.* or no matcher. Test without one, then narrow.
sessionStart cannot block a session even with continue: false; subagentStart treats ask as deny; beforeShellExecution is the event that can turn a command into a prompt.
Where loading goes wrong#
The project is not trusted, or the path is wrong. Project hooks run only in a trusted workspace, and relative paths start at the project root. If the folder is untrusted, the file is skipped; a path copied from a user hook points at the wrong place. Check both.
The plugin is installed but not registered. The reported editor failure was [HOOKS] Failed to parse project hooks configuration undefined in Developer Tools → Console, while the same plugin's rules and skills loaded. It reproduced on 3.2.21 with marketplace and local plugins; a user found the third-party toggle, and staff acknowledged it on 16 May 2026 as a workaround, not a fix for internal distribution (thread). Reload the window after installing. On the CLI side, the changelog for v2026.08.11 says installed plugin hooks, including --plugin-dir, now execute and refresh.
The plugin uses the wrong schema. A staff reply on 5 September 2026 says the shape that registered for plugin hooks was Claude's: PascalCase event, nested hooks array, type: "command". The plugin reference shows camelCase. A camelCase plugin hook that never registers is the reported failure; test both and watch the Hooks output (thread).
The Claude hooks are not being imported. Cursor loads .claude/settings.local.json, .claude/settings.json, and ~/.claude/settings.json only when Include Third-Party Plugins, Skills, and Other Configs is on under Settings → Agents → Third-Party Imports. It supports PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, PreCompact, and UserPromptSubmit; not Notification or PermissionRequest, and Claude's Glob has no Cursor mapping. Event and tool names are translated, and the translation is where the gaps are.
The hook came from a Claude plugin, or from Git Bash on Windows. Imported plugin hooks use another converter. A 28 September 2026 report found that a PreToolUse gate can fail open because it sees Shell not Bash, an empty cwd, dropped args, and failClosed: false; a Cursor staff reply treated it as unintended and gave workarounds (thread). On Windows, the CLI can build an imported hook as PowerShell but run it through bash when Git Bash sets MSYSTEM; every hook then dies on a syntax error, every tool call is rejected, and print mode still exits 0 (staff, 12 August 2026, CLI 2026.08.11; thread). Launch agent from PowerShell or cmd, or use a clean HOME and USERPROFILE.
The surface does not run that event. Cloud agents load project, team and enterprise hooks, not ~/.cursor/hooks.json; command hooks only; no sessionStart, sessionEnd, MCP hooks, Tab hooks or workspaceOpen; none during the read-only early turns. In September 2026, cloud agents were reported to miss subagentStart/subagentStop for subagents started in the first turn; staff confirmed that scope on 1 October 2026 and suggested a short warm-up message first (thread).
Events with known gaps#
These are reported behaviours, not a complete list. Check the date and your build before treating one as the explanation.
- Background
subagentStopdoes not fire. Foreground subagents emit start and stop; background ones emit start and then nothing. Staff confirmed it on 26 July 2026 and saidsummary,modified_files, andagent_transcript_pathwere also missing (thread). UsepreToolUse/postToolUseon theTasktool, or have the subagent write its own result file.
sessionStartcan miss the first chat in a window. If the window creates its first chat before the project hooks load, the step is skipped and never replayed; later chats work. Staff confirmed this on 24 October 2026 (thread). Wait for theLoaded N project hook(s)line, then start a new chat. Staff also suggest returningadditional_contextfrombeforeSubmitPrompt, but the field is not in its documented schema, so test it.
sessionEndis not the end of a turn. The docs and staff describe it as firing when a composer, tab, or window closes, not when a reply finishes; on 3.19.7 a reporter could not reproduce the composer/tab cases, so treat the trigger as build-dependent. A pluginsessionEndcommand on window close also failed withMainThreadShellExec not initialized(staff, 12 July 2026); the 5 September 2026 follow-up saw no spawn. Usestopfor an end-of-turn action (thread).
AskQuestionskipspreToolUseandpostToolUse. Staff confirmed on 29 May 2026 that the tool is not wired to either event in the IDE or the CLI, and there is no workaround (thread). Do not build a permission gate that assumes every tool call produces a hook event.
It ran, and the command still went through#
Exit code 0 uses the JSON; exit code 2 blocks; any other exit — crash, timeout, missing interpreter — fails open unless failClosed: true. Permission hooks block on invalid JSON or a schema mismatch even without it. A guardrail that logs cleanly and does nothing is usually a script that exits 1 before printing, or prints allow and exits 0. A timeout is a failure, not a deny. Use exit 2 for a hard block, set failClosed, and test the failure path.
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": ".cursor/hooks/check.sh",
"timeout": 10,
"failClosed": true
}
]
}
}Hooks are a guardrail, not a sandbox. On Windows, the desktop app runs hook commands through PowerShell and pipes JSON in; PowerShell reads $input, other programs read stdin.
The additional_context gap#
additional_context is documented on sessionStart, postToolUse, and postToolUseFailure; the other events do not take it. Delivery has been unreliable.
Staff said on 24 March 2026 that postToolUse ran fire-and-forget for non-MCP tools and discarded additional_context; only updated_mcp_tool_output was consumed for MCP (thread). On 20 April 2026, staff said sessionStart's additional_context was dropped by a timing issue, while env worked (thread). Tests on 3.7.27 and 3.7.36 on 15 June 2026 found input and synchronous execution fixed but the context still missing. The CLI changelog for v2026.10.17, in its note on post-tool hooks for subagents, says additional_context returned by a hook now reaches the main agent; it does not mention the sessionStart path.
Treat it as best effort. If the agent must know something, put it in a rule or AGENTS.md; a hook can enforce a boundary or record an event, not deliver instructions reliably. The rules guide and AGENTS.md guide cover the file routes.
A marker test shows what happened in one turn:
#!/bin/bash
input=$(cat)
printf '%s %s\n' "$(date -Is)" "$input" >> /tmp/cursor-hook.log
printf '%s\n' '{"additional_context":"Hook canary 7f3a. Begin your next reply with HOOK-7F3A."}'Put it in ~/.cursor/hooks/canary.sh and reference ./hooks/canary.sh, or in .cursor/hooks/canary.sh and reference that; register it under sessionStart in hooks.json. Open a new chat. The log proves the hook ran; the prefix is evidence the text reached the model. A missing prefix could mean it was dropped or ignored, so it is a test, not proof of the loader. Do not ask the agent whether the hook ran — it sees the prompt, not the hook service.
Check it in this order#
- Open Customize → Hooks (older builds call it Settings → Hooks) and find the entry, then read the load and execution lines in Output → Hooks.
- Open Developer Tools → Console and look for a parse error. Reload the window after fixing it; restart the app if the reload does not take.
- Make the script log its stdin before it does anything else. Run the event with a harmless action. No log line means the hook never started; a log line but no block means the problem is the response or the exit code, not the event.
- For a permission hook, deny a harmless
echo cursor-canary, then ask for it. If it runs, check the exit code,failClosed, and that the script is executable. - For a bug report, copy the Request ID from the chat menu, the Output → Hooks lines, and the script's log. Staff asked for exactly those in the subagent thread.
Where to go next#
- Cursor guides — the rest of the set.
- Why your Cursor rule isn't applied — when the rule itself needs enforcing.
- Why Cursor keeps asking for permission — when the prompt is the gate, not the hook.
- Cursor says MCP is connected, so why aren't the tools there? — when the tool never reaches the hook.
- Context compaction — when a rule loaded but disappeared after summarization.