Cursor
Cursor hooks: hooks.json, beforeShellExecution, afterFileEdit and exit codes
How do I configure hooks in Cursor to run scripts on agent events?
Create a hooks.json file at <project>/.cursor/hooks.json or ~/.cursor/hooks.json with "version": 1 and a "hooks" object that maps event names like afterFileEdit or beforeShellExecution to commands. Each script receives JSON on stdin and can print JSON on stdout, for example {"permission": "deny"} to block a shell command. Exit code 2 also blocks the action, and Cursor reloads hooks.json automatically when you save it.
Where hooks.json lives
Cursor reads hooks from several places. From highest to lowest priority:
- Enterprise (MDM): /Library/Application Support/Cursor/hooks.json on macOS, /etc/cursor/hooks.json on Linux and WSL, C:\ProgramData\Cursor\hooks.json on Windows.
- Team: set in the web dashboard and synced to members (Enterprise only).
- Project: <project-root>/.cursor/hooks.json
- User: ~/.cursor/hooks.json
Project hooks run from the project root, so use paths like .cursor/hooks/script.sh. User hooks run from ~/.cursor/, so use paths like ./hooks/script.sh.
File format and per-hook options
The top-level version must be a positive integer. Use 1. Each event holds a list of hook entries.
.cursor/hooks.json
{
"version": 1,
"hooks": {
"afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }]
}
}- command (required): script path or command.
- type: "command" (default) or "prompt".
- timeout: execution timeout in seconds.
- matcher: regex that filters when the hook runs. Empty or "*" matches everything.
- failClosed: when true, a crash, timeout, non-zero exit or missing output blocks the action. Default false.
- loop_limit: per-script limit on follow-ups for stop and subagentStop hooks. Default 5, null removes the cap.
Hook events
- Agent: sessionStart, sessionEnd, preToolUse, postToolUse, postToolUseFailure, subagentStart, subagentStop, beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, beforeReadFile, afterFileEdit, beforeSubmitPrompt, preCompact, stop, afterAgentResponse, afterAgentThought
- Tab: beforeTabFileRead, afterTabFileEdit
- App lifecycle: workspaceOpen
For beforeShellExecution, the matcher is tested against the full command string. For preToolUse, it is tested against the tool type, such as Shell, Read or Write.
Input, output and exit codes
Every hook gets JSON on stdin with common fields such as conversation_id, generation_id, model, hook_event_name, cursor_version, workspace_roots, user_email and transcript_path. Each event adds its own fields.
- beforeShellExecution input: command, cwd, sandbox. Output: permission ("allow", "deny" or "ask"), user_message, agent_message.
- afterFileEdit input: file_path and edits (a list of old_string and new_string pairs).
- preToolUse output: permission, user_message, agent_message, updated_input.
- beforeSubmitPrompt output: continue, user_message.
- stop output: followup_message.
- Exit 0: hook succeeded, Cursor uses the JSON output.
- Exit 2: block the action, same as returning permission "deny".
- Any other code: hook failed and the action proceeds (fail open), unless failClosed is true.
- For permission hooks, invalid JSON or output that does not match the schema blocks the action.
Example: block risky shell commands
.cursor/hooks.json
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": ".cursor/hooks/block-risky.sh", "matcher": "rm -rf|curl|wget" }
]
}
}.cursor/hooks/block-risky.sh
#!/bin/bash
cat > /dev/null
echo '{"permission": "deny", "user_message": "Blocked by project hook", "agent_message": "This command is not allowed in this repo. Ask the user to run it."}'
exit 0shell
chmod +x .cursor/hooks/block-risky.sh
Because the matcher only fires on matching commands, the script can simply deny. Use "ask" instead of "deny" if you want the user to approve.
Example: format files after agent edits
.cursor/hooks/format.sh
#!/bin/bash # Requires jq and prettier file=$(jq -r '.file_path') npx prettier --write "$file" > /dev/null 2>&1 exit 0
Wire it to afterFileEdit as shown above and make it executable with chmod +x. Cursor watches hooks config files and reloads them on save. If a hook does not load, check the Hooks output channel for errors, or restart Cursor.
Sources
More on Cursor
Other guides
Get the weekly agent stack update
New official MCP servers, spec changes and harness releases, checked against the source. One email a week, no fluff.
Reviewed Oct 9, 2026. Settings change often; the linked vendor docs are the source of truth.