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.

GoalOfficial hookRole
Format/lint after the agent edits a fileafterFileEditPost-process; stdin includes file_path and edits
Allow/deny risky shell commandsbeforeShellExecutionPre-exec permission: allow / deny / ask
Gate MCP toolsbeforeMCPExecutionSame permission contract as shell
Block sensitive readsbeforeReadFileDeny the read via permission
Separate Tab from AgentafterTabFileEdit / beforeTabFileReadTab-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": true on 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.

Sources