Cursor hooks: every event, the hooks.json format and working examples
By Jake Marsh, who builds Taskhold. Updated 2026-11-01. Facts about other tools checked 2026-11-01. 6 min read.
Short answer: Cursor hooks are scripts Cursor runs at points in the agent loop, such as before a shell command, after a file edit or when the agent stops. You list them in hooks.json (~/.cursor/ or .cursor/ in a project). Each gets JSON on stdin and can answer with JSON, for example permission deny or a followup_message. Exit code 2 blocks the action.
Hooks let you run your own code around Cursor's agent: block a risky command, format files after every edit, log what the agent ran, or keep it working until the tests pass. This page lists every event, the file format, what each hook can return, and a few examples you can paste. To build a hooks.json from tested recipes, use the Cursor hooks.json generator.
Everything here comes from Cursor's hooks documentation, checked on the date below. For the same idea in other agents, see Claude Code hooks and Gemini CLI hooks. To watch several agents at once, try the live board demo.
Where hooks.json lives
- User: ~/.cursor/hooks.json. Relative commands run from ~/.cursor/.
- Project: .cursor/hooks.json in the repo, committed with the code. Runs only in a trusted workspace; relative commands run from the project root.
- Team: set in the Cursor dashboard on Enterprise plans and synced to members.
- Enterprise: /Library/Application Support/Cursor/hooks.json on macOS, /etc/cursor/hooks.json on Linux, C:\ProgramData\Cursor\hooks.json on Windows.
Every matching hook from every level runs. When their answers differ, deny beats ask and ask beats allow, whichever file they came from. user_message and agent_message are joined together.
The file format
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": "./hooks/guard-shell.sh", "matcher": "rm -rf|git push --force", "timeout": 10 }
],
"afterFileEdit": [
{ "command": "./hooks/format.sh" }
]
}
}- command: a shell command, an absolute path or a path relative to the hooks.json location.
- matcher: a regex. For beforeShellExecution it is tested against the full command; for preToolUse against the tool name (Shell, Read, Write, Grep, Delete, Task or MCP:<tool>). Empty or * matches everything.
- timeout: seconds.
- failClosed: true makes a crash, timeout or empty output block the action. By default a broken hook lets the action through.
- loop_limit: for stop and subagentStop, how many automatic follow-ups are allowed (default 5, null for no limit).
- type: "prompt" runs a model prompt instead of a command (not in cloud agents).
Every agent event
| Event | When it runs | What it can return |
|---|---|---|
| sessionStart | A new conversation starts | env variables, additional_context |
| beforeSubmitPrompt | You hit send, before the request goes out | continue: false to block, user_message |
| preToolUse | Before any tool (Shell, Read, Write, MCP, Task...) | permission allow or deny, updated_input, messages |
| beforeShellExecution | Before a shell command | permission allow, deny or ask, messages |
| beforeMCPExecution | Before an MCP tool call | permission allow, deny or ask, messages |
| beforeReadFile | Before the agent reads a file | permission allow or deny |
| postToolUse | After a tool succeeds | additional_context, replaced MCP output |
| postToolUseFailure | After a tool fails, times out or is denied | additional_context |
| afterShellExecution | After a shell command, with its output | nothing (for logging) |
| afterMCPExecution | After an MCP tool call, with its result | nothing (for logging) |
| afterFileEdit | After the agent edits a file | nothing (run a formatter here) |
| subagentStart / subagentStop | Around a Task subagent | permission on start; followup_message on stop |
| preCompact | Before context compaction | user_message (it can't block) |
| afterAgentResponse / afterAgentThought | After a reply or a thinking block | nothing |
| stop | The agent loop ends (completed, aborted or error) | followup_message, sent as your next message |
| sessionEnd | The conversation ends | nothing |
Tab completions have their own two hooks (beforeTabFileRead, afterTabFileEdit), and workspaceOpen runs when a workspace opens.
Input and exit codes
Each hook gets one JSON object on stdin. Every event includes conversation_id, generation_id, hook_event_name, cursor_version, workspace_roots, user_email and transcript_path, plus its own fields: command and cwd for beforeShellExecution, file_path and edits for afterFileEdit, status and loop_count for stop.
- Exit 0: success. Cursor reads the JSON you print to stdout.
- Exit 2: block the action, the same as answering permission deny. Claude Code hooks use the same convention.
- Any other exit code: the hook failed and the action goes ahead, unless failClosed is true.
- Permission hooks (preToolUse, beforeShellExecution, beforeMCPExecution, beforeReadFile, subagentStart) block the action if they print invalid JSON, even without failClosed.
Example: block force pushes
#!/bin/bash
# ~/.cursor/hooks/guard-shell.sh (beforeShellExecution)
cmd=$(jq -r '.command')
if echo "$cmd" | grep -qE 'git push (-f|--force)'; then
echo '{"permission":"deny","user_message":"Force push blocked by a hook.","agent_message":"Force pushes are not allowed. Push normally or ask me."}'
exit 0
fi
echo '{"permission":"allow"}'Example: keep going until the tests pass
#!/bin/bash
# .cursor/hooks/tests-pass.sh (stop)
status=$(jq -r '.status')
[ "$status" = "completed" ] || { echo '{}'; exit 0; }
if ! bun test >/dev/null 2>&1; then
echo '{"followup_message":"The tests are failing. Run bun test, fix the failures, then stop."}'
else
echo '{}'
fiLimits
- Cloud agents run command hooks only, skip the user-level ~/.cursor/hooks.json, and don't run sessionStart, sessionEnd, the MCP execution hooks, the Tab hooks or workspaceOpen.
- Project hooks need a trusted workspace.
- On Windows the desktop app runs hook commands through PowerShell.
- preToolUse accepts ask in its schema but doesn't enforce it; use beforeShellExecution or beforeMCPExecution for ask.
Hooks and a shared task board
Hooks see one agent at a time. When several agents run at once, Taskhold puts all of their tasks on one live board: Cursor reports steps and progress through the Taskhold MCP tools, and asks you questions there instead of stopping in the chat. A stop hook covers the runs that end without a word; the ready-made version is on Cursor hook for Taskhold, and connecting Cursor takes one click on the Cursor setup page.
See how far along your coding agents are.
$12/month or $120/year after the trial. Card up front. Cancel before day 7 and you pay nothing. Every agent you run is included.
Questions
- Where do I put hooks.json for Cursor?
- ~/.cursor/hooks.json applies to every project for you. .cursor/hooks.json in a repo applies to that project and can be committed so the team shares it. Project hooks run only in a trusted workspace.
- How do I block a command with a Cursor hook?
- Use beforeShellExecution with a matcher for the command, and print {"permission":"deny"} or exit with code 2. Add user_message and agent_message to explain why to you and to the agent.
- Can a Cursor hook make the agent keep working?
- Yes. A stop hook can return followup_message, which Cursor sends as your next message. loop_limit, 5 by default, caps how many times that can happen in a row.
- Are Cursor hooks the same as Claude Code hooks?
- They are close but not the same. Both take JSON on stdin and treat exit code 2 as block, but the event names and output fields differ, so a Claude Code hook needs changes before Cursor can run it.
Sources
- Cursor docs: hooks, checked 2026-11-01
Related
Updated 2026-11-01. Something wrong or out of date? Email hello@greatwork.company.