Cursor Hooks: How to Guard the Agent Before Saves and Commits
To add agent-loop guards with Cursor Hooks, define them in /.cursor/hooks.json for a project or ~/.cursor/hooks.json for your user. Official event names are stage-specific (afterFileEdit, beforeShellExecution, and so on). There is no single hook literally named “pre-save.”
This article follows the official Cursor Hooks docs only: where files live, how post-edit hooks differ from shell hooks, and what failures actually block. No pricing or personal reviews.
Where do you put Hooks?
One-line answer: Share project policy in .cursor/hooks.json; keep machine-wide hooks in ~/.cursor/hooks.json.
- Project hooks:
/.cursor/hooks.json— that repo only. Commands run from the project root, so paths look like.cursor/hooks/format.sh. - User hooks:
~/.cursor/hooks.json— every workspace. Commands run from~/.cursor/, so paths look like./hooks/format.sh.
Hooks are spawned processes that speak JSON over stdio (command hooks by default), or prompt-based hooks that evaluate a natural-language condition. Agent hooks (Chat / Cmd+K), Tab hooks, and workspaceOpen are separate surfaces.
Cloud agents load command-based hooks from the repo’s .cursor/hooks.json. User hooks under ~/.cursor/hooks.json do not apply in cloud VMs. On Enterprise, team and enterprise-managed hooks can also run in the cloud.
Minimal project example:
{
"version": 1,
"hooks": {
"afterFileEdit": [
{ "command": ".cursor/hooks/format.sh" }
],
"beforeShellExecution": [
{
"command": ".cursor/hooks/approve-network.sh",
"timeout": 30,
"matcher": "curl|wget|nc"
}
]
}
}
Scripts need chmod +x. Cursor reloads hooks.json on save; if nothing runs, check the Hooks tab under Customize and the Hooks output channel.
How do “pre-save” and shell hooks differ?
One-line answer: There is no documented “pre-save” event—use afterFileEdit after edits and beforeShellExecution to gate shell commands.
| Goal | Official hook | Role |
|---|---|---|
| Format/lint after the agent edits a file | afterFileEdit | Post-process; stdin includes file_path and edits |
| Allow/deny risky shell commands | beforeShellExecution | Pre-exec permission: allow / deny / ask |
| Gate MCP tools | beforeMCPExecution | Same permission contract as shell |
| Block sensitive reads | beforeReadFile | Deny the read via permission |
| Separate Tab from Agent | afterTabFileEdit / beforeTabFileRead | Tab-only surface |
afterFileEdit is ideal for formatters, but its schema does not expose fields that undo or reject the edit itself. To stop commits, network calls, or deletes, use beforeShellExecution (optionally with a matcher on the command string) or the broader preToolUse. When you only care about the shell, prefer beforeShellExecution over preToolUse.
Example beforeShellExecution stdout:
{
"permission": "deny",
"user_message": "Network command blocked by a hook.",
"agent_message": "Use an approved tool instead of curl/wget."
}
Does a failure block the edit?
One-line answer: A failed afterFileEdit does not roll back an applied edit; blocking happens on before-hooks via permission: "deny", exit code 2, or failClosed: true.
Official exit-code behavior:
- Exit 0 — success. For permission hooks, invalid JSON/schema still blocks the action.
- Exit 2 — block (same as
permission: "deny"). - Other failures — fail-open by default (log and allow). Set
"failClosed": trueon security-critical hooks so crashes, timeouts, and non-zero exits also block.
Do not expect afterFileEdit to act as a save gate when a formatter fails. Use before-hooks for denial, and set failClosed: true when the hook is an enforcement boundary.
Docs also note that preToolUse accepts permission: "ask" in the schema but does not enforce it today—prefer beforeShellExecution / beforeMCPExecution when you need a real ask/deny gate.
Wrap-up
For pre-save/pre-commit style guards: ① choose location → ② pick the event (afterFileEdit vs beforeShellExecution) → ③ set the block contract (deny / exit 2 / failClosed). See the Cursor Hooks documentation for the full schema and examples.