Hooks are where an instruction turns into a mechanism. They fire on lifecycle events and come in two kinds: a command that receives the event as JSON on input and returns a decision on output, and a handler based on a request to the model. The difference from rules is fundamental: a rule advises, a hook executes always and identically. A rule can be outweighed by other text, more urgent or more detailed - a command hook has nothing to outweigh it, because it does not take part in the reasoning. A handler based on a request to the model carries no such guarantee: it judges for itself and is therefore closer to a rule than to a mechanism. That is exactly why they are trusted with what must work regardless of wording.
The exit-code semantics of a command are simple and worth knowing by heart. Zero means success. Two blocks the action. All other codes do not block by default - the behavior is fail-open, that is, if the handler fails, work continues. That default was chosen deliberately: a broken handler must not paralyze the editor for an entire team. For critical checks there is a separate flag that puts a hook into fail-closed mode: if the check did not run, the action does not happen.
Choosing between those two modes is the main design decision in working with hooks. A logging hook must survive an external system being unavailable: if the log collector is down, development should not stop, so fail-open is reasonable there. A policy hook that catches secrets or destructive commands must be fail-closed: an unchecked action is better left undone than done blindly. Mixing those roles in one handler is a sure way to get both stoppages out of nowhere and holes at the same time: one half of the job demands tolerance to failures, the other flatly forbids it.
It helps to see a definition in full once. Below is a hook on the event before a shell command runs: the type, the path to the handler, a timeout and an explicit fail-closed mode. The timeout is not a formality here: a handler that hangs longer than a human is willing to wait turns protection into irritation, and irritation sooner or later turns into protection switched off. Hence a practical requirement for a policy handler: the check must be local and fast, with no network calls, because the network is precisely the source of unpredictable delay.
It is worth picturing in advance how a mistake in the failure mode looks in the flesh. A handler that ships audit records to an external system is set to fail-closed so that no action goes unrecorded. A month later the external system goes down for maintenance, and the whole team discovers that the agent has stopped running any commands at all. The reverse case is quieter and worse: a handler that looks for secrets in edits is left fail-open, it dies on its very first line because of a typo in a path, and for six months nobody notices, because from the outside everything looks as before. A silent failure of protection is the more expensive of the two.
Hook sources have a strict priority: the enterprise level outranks the team one, the team outranks the project, the project outranks the user. Project paths execute from the project root. From this follows a practical rule: mandatory checks live at the top, where they cannot be removed locally, and convenient ones at the bottom, where they are easy to adjust. A check a person can switch off in their own file in ten seconds is not a policy, however strictly it is worded.
And the main warning: a hook is executable code that arrived together with the repository. Trusting it just because it sits in the project is not acceptable. It is read as code: what it runs, what dependencies it pulls, what it prints to output, whether content leaves the machine. Pinning dependency versions and limiting output matter here as much as in any other code you have agreed to execute. The only difference is that this code runs on every event and without a separate approval, which earns it more attention than usual rather than less.
It helps to understand the boundary between a hook and an allowlist of commands, because their jobs are neighbours. An allowlist answers the question of whether a command is familiar: the comparison is with text, the decision is binary, the setup is cheap. A hook answers whether it is acceptable in these circumstances: it sees the whole event and can take into account the branch, the directory, the content of the edit, the time of day. That expressiveness is paid for with code that has to be written, reviewed and maintained. The selection rule is simple: if a requirement can be expressed by enumeration, express it by enumeration, and reach for a hook when the decision depends on context.
The engineering conclusion is simple: hooks give determinism where the model's judgment is unreliable. A mandatory lint after an edit, a ban on a destructive command, an audit record - all of that runs identically for any wording of a request. But that reliability is paid for with attention to detail: the timeout, the failure mode, the process's rights and a review of the handler itself. And it is paid for with verification: a hook for which no negative scenario has been written counts as not working until proven otherwise.
The typical failures are predictable. Mixing audit and policy in one handler. Leaving a critical check in fail-open mode and not noticing it has not fired for years. Writing a hook without a timeout and getting a hang instead of protection. Putting a network call into a path that must finish in seconds. And accepting a hook from the repository as trusted without reading it.
// .cursor/hooks.json
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"type": "command",
"command": ".cursor/hooks/check-command.sh",
"timeout": 10,
"failClosed": true
}
]
}
}
// source priority: enterprise, team, project, user