Cursor
Installing the Agent Firewall for Cursor, how the format was verified, and what its hooks cannot see
The Cursor adapter translates Cursor's hook events (the Cursor CLI, cursor-agent, and the IDE's agent, local runs) to the firewall's normalized actions and its answers back. The policy is the same engine Claude Code uses.
How the format was verified
Read from source, not captured live. Cursor's agent cannot run without a Cursor account, so no live event was recorded. Instead, cursor-agent 2026.10.01 was downloaded into a scratch directory and its shipped JavaScript read (the hook executor, the hook schemas and validators, and the tool executors), and the result was checked against Cursor's hooks documentation (fetched 10 Oct 2026). The fixtures say so.
Not verified: the IDE (its agent is documented to use the same hooks; only the CLI's code was read), the names Cursor's server gives its own server-side tools (web search, to-dos, plans) in preToolUse, and whether a subagent's tool calls carry its subagent_id as their conversation.
Install
launchsafe-firewall install --agent cursor # ~/.cursor/hooks.json
launchsafe-firewall install --agent cursor --scope project # <project>/.cursor/hooks.json (see below)
launchsafe-firewall doctorinstall puts one entry per step first in each list, keeps every other hook, sets version: 1 when the file has none, saves the original under ~/.launchsafe-firewall/backups/, records the install and is idempotent. uninstall --agent cursor removes exactly the firewall's entries (and version when install added it); --restore-backup puts the file back as it was. Cursor does not load a hooks.json reached through a symlink, so install refuses one rather than write hooks that never run. Cursor reloads hooks.json on save; there is nothing to trust or enable for the user file.
Cursor's entries are flat (no matcher group; no matcher means every tool) and its timeouts are in seconds.
Fail-closed without a shell wrapper. Every entry says failClosed: true, so a crash, a non-zero exit other than 2, a timeout or an empty answer blocks the step. That is stronger than the Codex and Gemini CLI wrapper, because it covers a timeout. The command has no || exit 2 wrapper on purpose: Cursor's legacy payload transport appends a heredoc to the command, which a trailing group would take instead of node.
Project scope, with care. Cursor runs a project's .cursor/hooks.json only in a trusted workspace, and its cloud agents run it too, where this machine's node and firewall paths do not exist: the hook fails, and being fail-closed it blocks every action of a cloud agent in that repository. Prefer --scope user; doctor warns on a project install.
Whole-file validation. Cursor validates hooks.json as a whole and loads none of it on any error: a version that is not a positive integer, an unknown step name, or one malformed entry. doctor checks the file validates and is not a symlink, and an agent write that would break the file is denied (FW-TAMPER).
Managed deployment
launchsafe-firewall install --managed --agent cursor --print | sudo tee "/Library/Application Support/Cursor/hooks.json" # Linux: /etc/cursor/hooks.jsonThe enterprise file (MDM) and team hooks (Cursor's dashboard, Enterprise) run in every workspace, alongside user and project hooks. Cursor merges every hook's answer and a deny from any of them wins, but nothing stops other hooks from running too, so doctor scores this as partial.
What the adapter maps
| Cursor step | Firewall |
|---|---|
preToolUse | The decision, for every local tool (its deny is enforced; ask is ignored by Cursor) |
preToolUse for MCP:<tool> | Deferred: it carries no server, so beforeMCPExecution decides |
beforeMCPExecution | The decision for an MCP call, named by the server's mcp.json key |
postToolUse, afterMCPExecution | Taint, private data and fingerprints from the output |
beforeReadFile | Runs after a read and before the model sees it: the content is recorded like a tool's output; always allowed (the read was decided at preToolUse) |
beforeSubmitPrompt | Destinations you named, and the task's scope |
sessionStart | The session-start agent-configuration scan and MCP comparison |
subagentStart and subagentStop | Linkage: the subagent starts with the parent's state and hands its taint back |
beforeShellExecution, afterShellExecution | Understood, not registered (preToolUse and postToolUse cover the shell; registering both would decide every command twice) |
afterFileEdit, postToolUseFailure, stop, preCompact, afterAgentResponse, afterAgentThought, sessionEnd, Tab and workspaceOpen steps | Ignored |
| Cursor tool | Judged as |
|---|---|
Shell | A shell command (in cwd when it differs from the session's) |
Read, ReadLints | A read |
Write (every edit carries the whole new file) | A write, with settings files checked on the exact result. A Write without its text is held, never taken as an empty file |
Delete | A removal |
Grep, List | A search and a listing |
Fetch (and WebFetch) | A web fetch |
WebSearch, Task, updateTodos or TodoWrite, createPlan, askQuestion | Server-side tools, names taken from Cursor's docs and tool kinds, not seen in a hook event |
ListMcpResources, FetchMcpResource | MCP resource calls, plus a write of unknown content to download_path |
WriteShellStdin, ComputerUse, RecordScreen, anything else | An unknown tool: held for a person (FW-UNKNOWN-TOOL); the hook input does not say what is typed, clicked or recorded |
Output protocol (read from source)
- Permission steps (
preToolUse,beforeMCPExecution,beforeReadFile,subagentStart) get an allow or a deny with a user message and an agent message; exit 0. beforeSubmitPromptgets{"continue":true};postToolUseandsessionStartmay get additional context, kept under Cursor's 10,000 characters.- Exit 2 blocks. With
failClosed: true, any other failure blocks too. - No step lets the firewall ask you (
preToolUseandbeforeMCPExecutionact only on deny), so an action that needs a person is held in the queue and the agent gets thelaunchsafe-firewall queue approve <id>command.
Cursor runs the Claude Code hooks too
With "Include Third-Party Plugins, Skills, and Other Configs" on (the default), Cursor loads the hooks in ~/.claude/settings.json and a trusted project's .claude/settings*.json, and sends them its own payload. The firewall's Claude Code hook recognises any event carrying cursor_version and routes it to the Cursor adapter.
- When the firewall's Cursor hook runs for that step (registered, fail-closed, in
~/.cursor/hooks.jsonor the enterprise file, in a file Cursor accepts), the imported hook says allow and records nothing, so nothing is decided or counted twice. - Otherwise it decides in full. An MCP call there has no server and is judged as a call to an unknown server;
doctorrecommendsinstall --agent cursor.
Unattended runs
A background agent (when the event carries is_background_agent: true), cursor-agent -p or --force or --yolo in the process ancestry, and approvalMode: "unrestricted" in ~/.cursor/cli-config.json count as unattended: held actions wait in the queue, and the message says nobody was there.
What its hooks cannot do
- Cloud agents run only the repository's
.cursor/hooks.json(not yours), and a project install's local paths do not exist there. - Tab completions are your own edits; the firewall only observes them. Typed shell input (
WriteShellStdin), computer use and screen recording carry nothing the firewall can judge, so they are held. - No
ConfigChangestep: a change tohooks.jsonis judged when the agent makes it through a tool, not when something else edits the file. - Secrets in tool output are not redacted (Cursor can replace only an MCP tool's output).
- For what hooks cannot see, run Cursor's own sandbox (
sandbox.mode: "enabled"incli-config.json;doctorwarns when it is off) and an egress proxy.