LeastGrant

Generated from compatibility/cursor.json, the same record leastgrant doctor reads. Last verified 2026-09-02.

all agents

Cursor

Unproven REAL TRANSPORT PROBED 3.18.25 win32

the agent's own invocation was reproduced exactly and LeastGrant driven through it — stronger than reading the contract, and not the same as running the agent.

How it attaches

Per-step command hooks in ~/.cursor/hooks.json, on the three before* gates plus two after* events for learning. On Windows the payload is delivered through PowerShell from a temp file rather than on stdin.

Install it with leastgrant install cursor. Configuration is written to ~/.cursor/hooks.json, alongside anything already there — LeastGrant never removes a hook it did not add.

What a verdict does here

The three verdicts do not travel equally well. This is the table that decides whether LeastGrant is a prompt, a veto, or a suggestion on this agent.

verdictlands?what actually happens
allowyessource
askpartialsourceA real forced prompt on beforeShellExecution and beforeMCPExecution only, and there it beats the most permissive mode. On every other event there is no ask: on preToolUse an ask does not degrade quietly, it rejects the tool call outright with an error.
denyyessourceAccepted on beforeShellExecution, beforeMCPExecution, beforeReadFile, beforeTabFileRead and the subagent events, and it beats the most permissive approval mode.

Interactive and unattended

This agent does not tell the hook which mode it is in, so LeastGrant cannot distinguish an attended session from an unattended one and treats every session as unattended.

When the hook breaks

The question nobody asks until it matters: if LeastGrant crashes, times out, or cannot start, does the tool call still run?

  • On crash — the call is refused. (probe) Two independent reasons, and both are now measured rather than read. Invalid or schema-invalid JSON on a before* event is converted into a DENY by the response validator, and LeastGrant sets `failClosed` on those three events, which Cursor honours by refusing a call whose hook produced no output. Verified by driving Cursor’s real Windows transport: a hook that emits nothing has its tool call refused with "Tool blocked because this hook is configured to fail closed".
  • On timeout — the call is refused, after 60s by default. Follows from failClosed on the gating events. Per-script timeout defaults to 60s.
  • Can it be made to fail closed? — yes. A per-script `failClosed: true` option exists, and LeastGrant's installer now sets it on the three gating events (beforeShellExecution, beforeMCPExecution, beforeReadFile). Deliberately NOT set on the after* observation events: the command has already run by then, so refusing there rejects the result of completed work rather than preventing anything.

What it can see

A verdict is only worth as much as the set of actions it is asked about. gated means LeastGrant is consulted before the thing happens; seen after means it is told afterwards and can at best withhold the result; not covered means the action happens with LeastGrant never hearing about it.

tool classcoveragedetail
shell commandsgatedsource
file readsseen aftersourceNot a gate. beforeReadFile is fired from the Read tool's post-execution path with the file content already loaded, so a credential read has happened by the time LeastGrant sees it. A deny suppresses the content reaching the model; it does not prevent the read.
file writesnot coveredsourceThere is no before-write event. Writes are not intercepted.
deletionsnot coveredsource
MCP callsgatedsource
subagent spawngatedsource
network / webunknownunknown

What has actually been run

Four different things, deliberately not collapsed into one badge. Reproducing an agent's invocation is not running the agent; a passing conformance suite says our side is right and nothing about whether the host ever calls us.

  • Live agent test — not done. Cursor has no headless agent mode. `cursor --help` offers VS Code flags plus `--chat`, which opens a window, and the hooks service is a workbench contribution whose extension host is forked per window. A human has to drive a GUI. Three things stay unverified because of it: that Cursor loads ~/.cursor/hooks.json, that it registers the five steps, and that an ask raises the approval UI.
  • Real transport probed — Cursor’s Windows invocation reproduced byte for byte — powershell.exe with -NoProfile -NonInteractive -ExecutionPolicy Bypass, the payload written to a %TEMP% file and piped in through $input — and LeastGrant driven through it. This is how the UTF-8 BOM bug was found, which had LeastGrant refusing every tool call. 3.18.25 · win32 · 2026-09-02
  • Contract read from the shipped build — The 21-step event list, response validator, failClosed handling and the post-execution nature of beforeReadFile read from the shipped 42 MB workbench and extension-host bundles. 3.18.25 · win32 · 2026-09-02
  • Conformance suite — Driven by test/conformance.test.ts. 3.18.25 · win32 · 2026-09-02

Not exercised on darwin, linux.

What it cannot do

Not a disclaimer. The point of everything above is that this list exists and is specific.

  • File reads are observed after the fact, not gated. A deny stops the content reaching the model but the file has already been read.
  • Writes, edits and deletes have no pre-execution event and are not intercepted at all.
  • An ask on preToolUse is rejected as unsupported and the tool call errors, which is neither a prompt nor a clean degradation.
  • No permission mode reaches the hook, so LeastGrant cannot tell an attended session from an unattended one.
  • Cursor also ingests Claude Code hooks from .claude/settings.json and ~/.claude/settings.json, so installing both adapters may double-fire.
  • Never run inside a live Cursor session, and there is no way to do so autonomously: `cursor --help` offers only VS Code flags plus `--chat`, which opens a window, and the hooks service is a workbench contribution whose extension host is forked per window. What HAS been done is stronger than a contract test and weaker than a live session — Cursor’s Windows invocation was reproduced byte for byte (powershell.exe with -NoProfile -NonInteractive -ExecutionPolicy Bypass, the payload written to a %TEMP% file and piped in through $input) and LeastGrant driven through it. What is still unverified is that a real Cursor loads ~/.cursor/hooks.json, registers the five steps, and raises its approval UI on an ask.
  • That reproduction is how the worst bug in this integration was found: PowerShell 5.1 prefixes the payload with a UTF-8 BOM, JSON.parse threw, the hook exited silently, and failClosed turned that into a deny of every shell command, MCP call and file read. Fixed. It is recorded here because it is the strongest available argument that contract-testing an integration is not the same as running it.
  • Verified on Windows only. The POSIX transport passes the payload on native stdin instead and has not been exercised.