# Cursor Hooks Reference — Run Modes, the Classifier, and failClosed

Cursor's full hook set — preToolUse, beforeShellExecution, beforeMCPExecution and the rest — the contract, Run Modes, and why failClosed defaults false.

<div style="margin:8px 0 28px;padding:20px 22px;border:1px solid var(--line-2);border-radius:12px;background:var(--color-accent-light,#f0effe);">
  <p style="margin:0 0 12px;font-size:15px;line-height:1.6;color:var(--acp-text);"><strong>Just want the answer?</strong> A Cursor hook is a command registered in <code>hooks.json</code> that runs before or after an agent action. <code>beforeShellExecution</code> and <code>beforeMCPExecution</code> can return <code>permission: "deny"</code> to block the call; a crashed hook fails open unless it sets <code>failClosed: true</code>. One entry in <code>~/.cursor/hooks.json</code>:</p>
  <pre data-track="CursorHooks: Hero Config Copy" style="margin:0 0 12px;background:var(--color-surface,#faf9ff);border:1px solid var(--line-2);border-radius:8px;padding:12px 14px;overflow-x:auto;"><code>{
  "version": 1,
  "hooks": {
    "preToolUse": [ { "command": "node ~/.acp/govern.mjs" } ],
    "postToolUse": [ { "command": "node ~/.acp/govern.mjs" } ]
  }
}</code></pre>
  <p style="margin:0;font-size:13px;color:var(--acp-text-dim);"><a href="/integrations/cursor" data-track="CursorHooks: Setup Guide" style="font-weight:600;">Install the ACP hook in one command &rarr;</a> &nbsp;&middot;&nbsp; <a href="/controls/cursor" data-track="CursorHooks: Controls Page">Cursor's full control model &rarr;</a> &nbsp;&middot;&nbsp; free up to 5 agents</p>
</div>

Cursor's permission story is the one that's changed the most this year — yolo mode became Auto-Run became Run Modes, two modes were deleted in May, and an LLM classifier became the recommended default a week later. Hooks held still through all of it: a rich event set, a documented input/output contract, and deterministic once a hook actually runs. This is the reference for what's in `hooks.json`, what each event receives and returns, and the one default worth knowing before you rely on it.

## Where hooks live

`hooks.json` is read at four layers, highest wins on conflict, all matching hooks run:

| Scope | Location |
|---|---|
| Enterprise (MDM) | `/Library/Application Support/Cursor/hooks.json` (macOS), `/etc/cursor/hooks.json` (Linux/WSL), `C:\ProgramData\Cursor\hooks.json` (Windows) |
| Team | Web dashboard, Enterprise only, synced roughly every 30 minutes |
| Project | `<project-root>/.cursor/hooks.json`, requires a trusted workspace |
| User | `~/.cursor/hooks.json` — where the ACP installer writes |

<!-- source: https://cursor.com/docs/hooks -->

Top-level shape: a `version` number (default `1`) and a `hooks` object mapping event names to arrays of definitions. Each definition takes `command` (required), `type` (`"command"` by default, or `"prompt"`), `timeout` in seconds, `matcher`, and `failClosed`. Project hooks run from the project root; user hooks run from `~/.cursor/`.

## The events

Cursor's docs list a longer event set than most harnesses ship. The agent-facing ones, in loop order:

`sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `subagentStart`, `subagentStop`, `beforeShellExecution`, `afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `beforeReadFile`, `afterFileEdit`, `beforeSubmitPrompt`, `preCompact`, `stop`, `afterAgentResponse`, `afterAgentThought`.

Two more live outside the agent loop: `beforeTabFileRead` and `afterTabFileEdit` apply to inline Tab completions specifically, not Cmd+K or Agent Chat; `workspaceOpen` fires when a workspace opens or its folders change, with no session, conversation, or model fields in its payload at all.

For control, the shell and MCP pair matter most: `beforeShellExecution` and `beforeMCPExecution` are the hooks that can actually stop a call before it runs, with a full `permission: allow | deny | ask` contract. `preToolUse` fires for every tool and can rewrite input via `updated_input`, but its `ask` value is accepted by the schema and not enforced — treat it as observe-and-rewrite, not observe-and-hold.

<!-- source: https://cursor.com/docs/hooks -->

## The contracts that hold

**`beforeShellExecution`** — in: `command`, `cwd`, `sandbox`. Out: `permission` (`allow`/`deny`/`ask`), `user_message`, `agent_message`.

**`beforeMCPExecution`** — in: `tool_name`, `tool_input` (a JSON string), `mcp_server_name`, plus `url` / `mcp_server_url` for HTTP/SSE servers or `command` for stdio ones. Out: the same `permission` shape. The docs advise treating a missing or unexpected `mcp_server_name` as a deny.

**`preToolUse`** — in: `tool_name`, `tool_input`, `tool_use_id`, `cwd`, `agent_message`. Out: `permission` (`allow`/`deny`; `ask` accepted, not enforced), `user_message`, `agent_message`, `updated_input`.

**`postToolUse`** — in: `tool_name`, `tool_input`, `tool_output` (JSON-stringified), `tool_use_id`, `cwd`, `duration`. Out: `updated_mcp_tool_output` (MCP calls only), `additional_context`.

**`beforeReadFile`** — in: `file_path`, `content`, `attachments`. Out: `permission` (`allow`/`deny`). Fail-open by default, same as the rest, unless the hook sets `failClosed: true`.

**Exit codes.** `0` with JSON on stdout is the normal path. `2` blocks the same way a `permission: "deny"` would. Any other exit code is treated as no decision — fail-open.

<!-- source: https://cursor.com/docs/hooks -->

## Matchers

A `matcher` filters when a hook fires, and the field it matches against depends on the event: `preToolUse`/`postToolUse`/`postToolUseFailure` match a tool type — `Shell`, `Read`, `Write`, `Grep`, `Delete`, `Task`, or `MCP:<tool_name>`; `beforeShellExecution`/`afterShellExecution` match the full command string; `subagentStart`/`subagentStop` match the subagent type. No matcher means the hook sees every call for that event.

## Common input, and the `prompt` hook type

Every hook payload (`workspaceOpen` excepted) carries `conversation_id`, `generation_id`, `model`, `model_id`, `hook_event_name`, `cursor_version`, `workspace_roots`, `user_email`, `transcript_path`. A `sessionStart` hook can return an `env` object that propagates to later hooks in the session.

Cursor also ships a second hook type beyond `command`: `type: "prompt"` runs a fast LLM against a natural-language condition and returns `{ ok: boolean, reason }`, with `$ARGUMENTS` substituted for the hook's input JSON. That's a distinct mechanism from the Run Modes classifier below — a prompt hook is one entry you write and register yourself; the classifier is a permission-layer default that evaluates every unmatched call.

<!-- source: https://cursor.com/docs/hooks -->

## `failClosed`: the default worth designing around

Every event above shares one behavior: **`failClosed` defaults to `false`.** A crashed hook, a timeout, or invalid JSON on stdout doesn't hold the call — it lets it through, same as if no hook were registered at all. If a hook is your enforcement — a deny rule, a policy check, an approval gate — that default means an outage in the hook's own dependencies silently turns it off. [Cursor's docs recommend](/controls/cursor) setting `failClosed: true` on security-critical `beforeMCPExecution` hooks, and it's also documented on `beforeReadFile` — nothing forces you to set it on the rest.

This is the same shape of question every hook-based control answers differently. [Claude Code's PreToolUse hooks](/blog/claude-code-hooks-reference) treat a timeout as no decision too, letting the call run under `bypassPermissions`. Cursor's difference is that the fail-open default is explicit and per-hook, not implicit and mode-wide — a setting you choose, not a mode you're stuck in.

## Run Modes and the classifier

Hooks are one layer of Cursor's control stack; the permission posture around them is a **Run Mode**, and it's worth knowing what a hook's `ask` is competing against:

1. **Auto-review** (recommended default). Allowlisted calls run. Other shell commands run sandboxed where possible. Whatever's left goes to an LLM classifier — a small, Cursor-managed model — that returns allow, try-something-different, or ask. It covers shell, MCP, and fetch.
2. **Allowlist.** Listed actions run; everything else prompts. No classifier, sandbox optional.
3. **Run Everything.** Everything auto-runs — no sandbox, no classifier, no prompts.

The classifier is a judgment call, not a rule — [Cursor's own docs say the allowlist beneath it is "best-effort, not a security boundary."](/controls/cursor) A `beforeShellExecution` or `beforeMCPExecution` hook that returns `deny` sits upstream of all three modes and holds regardless of which one is active; a `preToolUse` `ask` does not, since it isn't enforced.

<!-- source: https://cursor.com/docs/hooks ; https://agenticcontrolplane.com/controls/cursor -->

## What the ACP hook does with the same contract

The [ACP integration for Cursor](/integrations/cursor) installs the same `govern.mjs` script Claude Code and Codex use, registered under both `preToolUse` and `postToolUse` in `~/.cursor/hooks.json`. `preToolUse` POSTs to `/govern/tool-use` and returns `permissionDecision: "deny"` to block; `postToolUse` POSTs to `/govern/tool-output` and writes the audit-log entry, surfacing PII and secret findings. The installer detects Cursor by `~/.cursor` existing or `cursor` on PATH, is idempotent, and is safe to re-run.

Two honest limits: Cursor's hook API is newer and less stable than Claude Code's, so field names have moved and may move again; and `postToolUse` output mutation is observe-only today — ACP can flag findings into the audit log but can't rewrite tool output before Cursor sees it.

## No `--dangerously-skip-permissions` — but YOLO mode exists

Cursor has no equivalent flag to Claude Code's bypass mode. **YOLO mode** (Run Everything, functionally) can auto-run tools without prompting, but hooks still fire under it — a `beforeShellExecution` deny holds in Run Everything the same as it does in Auto-review. [Antigravity's comparable flag removes the interception surface entirely](/blog/antigravity-hooks-reference); Cursor's doesn't.

## Known limitations

- **`preToolUse`'s `ask` isn't enforced.** If a hook needs to hold a call for human review, use `beforeShellExecution` or `beforeMCPExecution`, both of which honor `ask` fully.
- **`failClosed` is opt-in, per hook.** A crashed hook fails open by default; a hook you're relying on for enforcement needs the flag set explicitly.
- **The allowlist beneath the classifier is best-effort.** Cursor's own docs name three published CVEs of allowlist bypass. A hook's `deny` is deterministic where the classifier's verdict is judgment.
- **Cloud agents only read project hooks.** `.cursor/hooks.json` at the repo root applies; `~/.cursor/hooks.json` does not reach a cloud agent run.

## Comparing to Claude Code and Antigravity hooks

Claude Code's `PreToolUse` contract is the closest sibling — same exit-code-2-or-JSON shape, same `matcher` idea — but Claude Code's hooks fire in every permission mode including its bypass flag, where Cursor has none. [The Claude Code hooks reference](/blog/claude-code-hooks-reference) has that contract in full; [the Antigravity hooks reference](/blog/antigravity-hooks-reference) covers the fail-closed core and the native `force_ask` decision, which neither Claude Code nor Cursor has.

## Frequently asked questions



## Where to read more

- [Cursor hooks docs](https://cursor.com/docs/hooks) &mdash; the upstream reference this page checks against
- [Cursor's full control model, explained](/controls/cursor) &mdash; Run Modes, the sandbox, the allowlist CVEs, and where the model ends
- [Install ACP for Cursor](/integrations/cursor) &mdash; the hook, what it writes, and its documented limits
- [Cursor cost tracking reference](/blog/cursor-cost-tracking) &mdash; the other plane: what hooks can't see
- [Claude Code hooks reference](/blog/claude-code-hooks-reference) &mdash; the closest sibling contract
- [Codex CLI hooks reference](/blog/codex-cli-hooks-reference) &mdash; a narrower, shell-first hook surface
