Cursor rules are context the model reads; Cursor hooks are scripts that run in the agent loop. Per Cursor's rules docs, "Rules provide persistent, reusable context at the prompt level." Per Cursor's hooks docs, "Hooks let you observe, control, and extend the agent loop using custom scripts," and they "can observe, block, or modify behavior." Use a rule to tell the agent how to work and why. Use a hook when a check has to run on a specific action, such as denying a shell command that matches a pattern.
Side by side
| Rules | Hooks | |
|---|---|---|
| What it is | Persistent context at the prompt level | Spawned processes that talk JSON over stdio |
| When it applies | Always Apply, Apply Intelligently, Apply to Specific Files, or Apply Manually (@-mention) | On agent events such as `preToolUse`, `beforeShellExecution`, `beforeMCPExecution`, `afterFileEdit`, `stop` |
| What it can do | Inform the agent | Allow, deny or (on some hooks) ask; inject context; run formatters or audits |
| Where it lives | Project rules files | `hooks.json` at project (`.cursor/hooks.json`) or user (`~/.cursor/hooks.json`) level, or via plugins; Enterprise and Team sources too |
All rows are from Cursor's rules and hooks pages.
A hook that denies a command
Cursor's docs show `beforeShellExecution` with a `matcher`, which for that hook "is matched against the full command string":
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": ".cursor/hooks/block-git.sh", "matcher": "git" }
]
}
}The script reads JSON on stdin and returns `{"permission": "deny", ...}` to block, or exits with code `2`, which the docs describe as equivalent to returning `permission: "deny"`.
How failures behave
From Cursor's hooks docs:
- Exit code `0`: the hook's JSON output is used. For permission hooks, "invalid JSON or a response that doesn't match the hook's schema blocks the action."
- Exit code `2`: blocks the action.
- Other exit codes: "Hook failed, action proceeds (fail-open by default)."
- A per-hook `failClosed: true` option makes crashes, timeouts and non-zero exits block instead. The docs call it "Useful for security-critical hooks."
When several sources define hooks, "All matching hooks from every source run," and "any `deny` wins over `ask`, and `ask` wins over `allow`, regardless of source." For `preToolUse`, `"ask"` "is accepted by the schema but not enforced" today.
Using both
They aren't alternatives. A rule explains the policy (for example, "use `gh` instead of raw `git`"), so the agent plans around it. A hook runs on the action itself. Rules and skills cover guidance; see rules vs skills and skill vs rule.
Pitfalls
- Wrong relative paths. Project hooks run from the project root, so use `.cursor/hooks/script.sh`, not `./hooks/script.sh`.
- Expecting `ask` on `preToolUse`. It isn't enforced there today.
- Assuming every hook runs in cloud agents. Cursor's docs list hooks that don't, including `sessionStart`, `sessionEnd` and `beforeMCPExecution`/`afterMCPExecution`, and user-level hooks aren't available there.
- Not deciding on failure mode. Hooks fail open by default; set `failClosed` where you want the opposite.
How to check it
Open the Hooks tab in Customize or the Hooks output channel, which Cursor's docs point to for debugging. Trigger a matching command and confirm the deny. Then make the script exit with code `1` and confirm what happens with and without `failClosed`. If a skill isn't picking up, see skill not loading.
Where the kit fits
The Operator Pack ($47) is for teams setting up rules, skills and agent guardrails in Cursor. You can also start at /free. Cheat Sheet for the skim checklist mindset.
Soft next step
Write the policy as a rule first, then add a hook only for the actions that need a check on every run. Soft ladder: /free, then the Operator Pack ($47). Related: rules vs skills.