Cursor

Installing the Agent Firewall for Cursor, how the format was verified, and what its hooks cannot see

Edit on GitHub

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 doctor

install 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.json

The 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 stepFirewall
preToolUseThe 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
beforeMCPExecutionThe decision for an MCP call, named by the server's mcp.json key
postToolUse, afterMCPExecutionTaint, private data and fingerprints from the output
beforeReadFileRuns 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)
beforeSubmitPromptDestinations you named, and the task's scope
sessionStartThe session-start agent-configuration scan and MCP comparison
subagentStart and subagentStopLinkage: the subagent starts with the parent's state and hands its taint back
beforeShellExecution, afterShellExecutionUnderstood, 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 stepsIgnored
Cursor toolJudged as
ShellA shell command (in cwd when it differs from the session's)
Read, ReadLintsA 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
DeleteA removal
Grep, ListA search and a listing
Fetch (and WebFetch)A web fetch
WebSearch, Task, updateTodos or TodoWrite, createPlan, askQuestionServer-side tools, names taken from Cursor's docs and tool kinds, not seen in a hook event
ListMcpResources, FetchMcpResourceMCP resource calls, plus a write of unknown content to download_path
WriteShellStdin, ComputerUse, RecordScreen, anything elseAn 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.
  • beforeSubmitPrompt gets {"continue":true}; postToolUse and sessionStart may 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 (preToolUse and beforeMCPExecution act only on deny), so an action that needs a person is held in the queue and the agent gets the launchsafe-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.json or 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; doctor recommends install --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 ConfigChange step: a change to hooks.json is 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" in cli-config.json; doctor warns when it is off) and an egress proxy.

On this page