LeastGrant

compatibility

What survives the trip to your agent

The same policy runs everywhere. What reaches you at the other end does not. One agent turns an unanswerable question into a prompt, another into a refusal, and a third cannot see the tool call at all. This page says which, for each one, and how we know.

read the gaps, not the ticks

This is the page most likely to talk you out of using LeastGrant with your agent, and it is written that way on purpose. Finding out after you install that your editor has no event for file writes is worse than not installing.

Generated from compatibility/ in the repository. Nothing here is typed by hand.
Agent Overall allow ask deny on hook error shellreadswritesdeletesMCP
Claude Code 2.1.240 partial yesprobe degradesprobe yesprobe runs anywayprobe gatedprobegatedprobegatedprobegatedprobegatedprobe
Codex CLI 0.152.0 degraded ignoredsource nonesource yesprobe runs anywaysource gatedprobegatedprobegatedprobegatedsourcegatedsource
Cursor 3.18.25 partial yesprobe partialprobe yesprobe refusesprobe gatedprobegatedprobegatedprobegatedprobegatedsource
GitHub Copilot CLI 1.0.82 partial yesprobe degradesprobe yesprobe refusesprobe gatedprobegatedprobegatedprobeunknownunknownunknownunknown
Google Antigravity 2.11.0 partial yesprobe partialprobe yesprobe refusesprobe gatedprobegatedprobegatedprobegatedsourcegatedprobe
OpenCode 1.18.26 none — — — — —————

How the evidence is graded

Every graded cell in the table above carries how it was established, under the label, because the grades are not interchangeable and the difference has already mattered here.

  • probed — someone ran the real agent and watched this happen.
  • read — someone read the shipped binary. Strong evidence about the contract, none at all about the integration.
  • documented — the vendor says so. Weakest, and it never alone justifies the word verified: Cursor's own documentation says its hooks fail open by default, and reading what ships shows that depends on the failure kind.
  • unknown — nobody has checked. Printed as unknown rather than left blank, because a blank reads as fine.

partial

Claude Code

Verified against Claude Code 2.1.240 on win32. Untested on darwin, linux.

  • deny is enforced, including in the most permissive mode
  • an ask degrades — reaches a human when a human is there.
  • if the hook errors or times out, the call runs anyway — a LeastGrant that cannot start enforces nothing
  • verified against Claude Code 2.1.240 on win32

What the agent itself cannot do

  • Managed policy disableAllHooks: true switches every hook off, including managed ones, with no signal LeastGrant can see. It is silently absent.
  • Fails open on crash and on timeout, so a LeastGrant that cannot start enforces nothing.
  • A hook ask in non-interactive mode is a deny, not a prompt.
  • The public docs list PreModelSwitch and PostModelSwitch, which do not exist in the installed 2.1.240 binary.

What LeastGrant has not done yet

  • Only PreToolUse and PostToolUse are registered, so failed calls are never observed and their pending entries are never resolved.
  • Verified on Windows only. macOS and Linux run the same code path but nobody has driven the real binary there.

Last checked 2026-09-02 · adapter src/adapters/claude-code/hook.ts

degraded

Codex CLI

Verified against Codex CLI 0.152.0 on win32. Untested on darwin, linux.

  • deny is enforced, including in the most permissive mode
  • no ask exists here, so LeastGrant is a veto rather than a prompt
  • if the hook errors or times out, the call runs anyway — a LeastGrant that cannot start enforces nothing
  • verified against Codex CLI 0.152.0 on win32

What the agent itself cannot do

  • No ask verdict exists at all. On Codex, LeastGrant is a veto rather than a prompt.
  • A handler declared "async": true cannot apply control effects: its deny, its exit-2 block and its updatedInput are all silently ignored. A single wrong word in the config is a total, unsignalled bypass.
  • Hook trust is bound to a hash of the definition, so upgrading or reinstalling re-flags the hook and it stops running until re-trusted in /hooks.
  • On Windows the trust hash covers only commandWindows and on POSIX only command, so one hooks.json has different hashes on different machines.
  • codex exec has no trust review and no warning: an untrusted or modified handler is simply never added to the handler set, and nothing says so.
  • Fails open on crash and on timeout.
  • [features] hooks = false and allow_managed_hooks_only = true disable user hooks entirely.

What LeastGrant has not done yet

  • PermissionRequest has never fired in a real session. codex exec runs unattended, where by definition nothing prompts, so that path is contract-tested only.
  • Verified on Windows only.

Last checked 2026-09-02 · adapter src/adapters/codex/hook.ts

partial

Cursor

Verified against Cursor 3.18.25 on win32. Untested on darwin, linux.

  • deny is enforced, including in the most permissive mode
  • an ask reaches a person only on some channels — real on two surfaces and absent on two others, and the difference decides how everything else is mapped.
  • if the hook errors, the call is refused rather than run
  • verified against Cursor 3.18.25 on win32

What the agent itself cannot do

  • preToolUse accepts ask and silently allows the action. Cursor logs the response as valid and merges it, no prompt appears, and the tool runs. Only allow and deny are enforced on that surface, so LeastGrant refuses a floored file action rather than pretending a person was consulted.
  • beforeReadFile is post-execution: its payload carries the file content, so a deny there suppresses what reaches the model without preventing the read. It is kept as a second layer behind preToolUse, which does prevent it.
  • No permission mode reaches the hook, so LeastGrant cannot tell an attended session from an unattended one.
  • Cursor has its own sandbox and its own auto-run flow, both acting before the user sees anything. A command may be attempted sandboxed, blocked by that sandbox, and retried unsandboxed, with LeastGrant consulted separately each time.
  • Cursor also ingests Claude Code hooks from .claude/settings.json and ~/.claude/settings.json, so installing both adapters may double-fire.
  • Cursor’s generic step is spelled preToolUse and Claude Code’s is PreToolUse. Case-folded they are the same string, so a shared dispatcher will answer one in the other’s wire format unless it routes on the agent first.
  • There is no equivalent of Antigravity’s force_ask: nothing LeastGrant can return compels a prompt the host’s own settings cannot satisfy.
  • preToolUse has no ask, so any verdict the engine will not auto-approve — gap.blast, a tainted session, a previously refused action — becomes a refusal rather than a prompt. Only allow and deny are enforced there.

What LeastGrant has not done yet

  • MCP has never been exercised live. beforeMCPExecution is registered and was never invoked, so its behaviour is contract-derived.
  • The generic gate is scoped by matcher to the file tools. Cursor was observed sending Read, Write and Delete; the matcher also covers Edit, MultiEdit, ApplyPatch, StrReplace, SearchReplace, CreateFile, DeleteFile and the lower-case spellings read_file, write_file, delete_file, edit_file and search_replace, none of which have been observed and all of which are covered against a rename upstream rather than because they were seen. Move and Rename are deliberately NOT in the matcher: neither maps to a known tool kind, so gating them would refuse every move and rename rather than judge it.
  • Verified on Windows only. The POSIX transport passes the payload on native stdin rather than through PowerShell and has not been exercised.
  • The hook timeout was not tested; only the crash path was.
  • Agent-initiated deletes through Cursor’s file tools are refused outright, not prompted. The engine says every delete needs a person and this surface cannot produce one, so refusing is the honest translation — but it is stricter than before the generic gate existed, when deletes were invisible. The refusal message points at the terminal, where a prompt does appear.

Last checked 2026-09-03 · adapter src/adapters/cursor/hook.ts

partial

GitHub Copilot CLI

Verified against GitHub Copilot CLI 1.0.82 on win32. Untested on darwin, linux.

  • deny is enforced, including in the most permissive mode
  • an ask degrades — honoured interactively.
  • if the hook errors, the call is refused rather than run
  • verified against GitHub Copilot CLI 1.0.82 on win32

What the agent itself cannot do

  • No permission mode is sent, so LeastGrant cannot tell an attended run from an unattended one and records observation rather than approval. Copilot therefore learns more slowly, deliberately.
  • Failing closed means a broken or missing LeastGrant blocks all work rather than silently permitting it. Safer, and much louder.

What LeastGrant has not done yet

  • Speaks Claude Code's wire format, so it shares that adapter. A divergence upstream would surface as Copilot-shaped bugs in a file named for Claude.
  • This file's unknown fields need a dedicated binary read; the research pass for it did not complete.
  • Verified on Windows only.

Last checked 2026-09-01 · adapter src/adapters/claude-code/hook.ts

partial

Google Antigravity

Verified against Google Antigravity 2.11.0 on win32. Untested on darwin, linux.

  • deny is enforced, including in the most permissive mode
  • an ask reaches a person only on some channels — the verdict lands, but whether it reaches a person depends on the host.
  • if the hook errors, the call is refused rather than run
  • verified against Google Antigravity 2.11.0 on win32

What the agent itself cannot do

  • The hook engine installs only when the server-delivered experiment flag json-hooks-enabled is true. It is per-session, not persisted, not readable and not overridable from the client, so LeastGrant cannot tell an enforcing session from an unenforced one.
  • An ordinary ask does not necessarily reach a person. Observed live: with autoExecutionPolicy = CASCADE_COMMANDS_AUTO_EXECUTION_EAGER, an ask on a shell command and on a file write both executed with no prompt. Only force_ask is reliable, which is why LeastGrant floors to it.
  • force_ask is silently downgraded to an allow when the host sets auto_interaction_behavior=ALLOW_ALL. ResolveAutoInteraction runs before any prompt is registered and the hook gets no signal. Not exercised live; read from the runtime.
  • An empty stdout with exit 0 lets the call through. The runtime trims the output and treats an empty result as "no hook result", so a handler that stands aside silently is an allow. Every other failure mode blocks.
  • The runtime reads hooks from four workspace roots — .agents, _agents, .agent and _agent — and from plugins/<name>/hooks.json. Any of them can add a handler; the published docs mention only .agents.
  • Two of the six accepted verdicts are undocumented. auto_approve is accepted by the runtime and returns a silent allow with no prompt, appearing in neither the published docs nor the hooks guide embedded in the binary; deny_unless_prior_grant appears on the web page but not in that embedded guide.
  • deny and ask do not have the same reach. deny is enforced in the tool-call converter, before the permission manager; ask and force_ask are read only by the permissions package, which a step reaches only if it declares permission targets.
  • The runtime supports six hook events — PreToolUse, PostToolUse, PreInvocation, PostInvocation, Stop and SessionStart. Stop carries its own decision field. LeastGrant registers only the two tool events, so the others are unmediated.
  • Prompt-mode handlers exist (HookHandlerConfig.prompt, executePromptModeHook) despite the embedded guide saying only command handlers are supported. A prompt handler is a model deciding a permission question.
  • An unanswered force_ask blocks its conversation indefinitely. That is the intended behaviour of the verdict and it is also a way to wedge a session.

What LeastGrant has not done yet

  • Subagent tools are not verified. invoke_subagent, define_subagent and manage_subagents exist in the runtime, but no subagent call was made during the live run, so LeastGrant has not been observed gating one.
  • LeastGrant does not model the PreInvocation, PostInvocation, Stop or SessionStart events, and does not use the overwrite field that lets a hook rewrite a tool call’s arguments.
  • Verified on Windows only, through the Desktop Hub. The Unix launch form (sh -c) is not present in this build — GOOS is a compile-time constant — and the IDE surface was not exercised.
  • Network and web tools are unverified. The model answered a search request from its own knowledge rather than calling a tool, so search_web and read_url_content have never been observed reaching the hook.
  • A workspace on a disconnected drive stalls every tool call. A single blocking stat against an absent Windows device costs about 21 seconds and Node cannot bound a synchronous one, so the hook exceeds its timeout and the host blocks the call. Safe direction, unusable agent — and Antigravity itself would be no better off on that drive.
  • The shell parser reads a command line with POSIX rules, and Antigravity runs a Windows shell. An unquoted native path — a Windows credential path written with backslashes — has its backslashes eaten as escapes, so no path target is produced and every path-keyed floor goes quiet together. Quoting it, or spelling it with forward slashes, floors correctly. This affects every agent LeastGrant supports on Windows, not just this one.

Last checked 2026-09-03 · adapter src/adapters/antigravity/hook.ts

none

OpenCode

Verified against OpenCode 1.18.26 on win32. Untested on darwin, linux.

What the agent itself cannot do

  • permission.ask is declared in the plugin types and dispatched nowhere — trigger("permission.ask" occurs zero times in the shipped bundle. Building on it would produce an adapter that appears installed and enforces nothing.
  • There is no ctx.permission.hook("evaluate") in 1.18.26 or in the published 1.18.27. The v2 PluginContext exposes no permission domain, and no documentation page mentions one. The internal evaluate is a pure function over a ruleset with no extension point.
  • POST /session/{id}/shell executes a shell command without calling tool.execute.before or any permission gate, and records the result in session history as a tool part named "bash".
  • Command-template interpolation executes before command.execute.before, which therefore cannot prevent it and cannot even see the command — by the time it fires, its input holds the command’s output.
  • $ARGUMENTS is substituted into a command template before the shell section is extracted, so caller-supplied arguments are shell-interpreted.
  • GET /file/content returns file bytes with no hook and no permission event, and its directory parameter relocates the root outside the session’s project directory.
  • The local server is unauthenticated by default. HTTP Basic is available only when OPENCODE_SERVER_PASSWORD is set; without it the process prints a warning and serves anyway.
  • PermissionV2.Source admits exactly one shape, {type:"tool"}. Execution that is not a tool call cannot be expressed as a permission request at all, so the boundary cannot be widened from outside.
  • Plugins are loaded at startup from the config directory and run above the policy layer, so an agent able to write a plugin file, add a plugin dependency, or register a stdio MCP server outranks any adapter.
  • The TUI’s auto mode auto-replies "once" to every permission request, and replies are first-writer-wins over the same unauthenticated route.
  • Two permission stacks ship in one binary. PermissionV2 has its own assert/ask/reply with sqlite-persisted grants while the V1 path still serves ordinary sessions, and a "always" reply re-evaluates and silently resolves other pending requests in the session.

What LeastGrant has not done yet

  • No adapter ships. This file records a completed evaluation, not a supported integration.
  • Revisit if OpenCode wires permission.ask to the permission engine, which would make a faithful adapter straightforward.

Last checked 2026-09-03 · no adapter ships