LeastGrant

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

all agents

Claude Code

Partial LIVE VERIFIED 2.1.240 win32

the real agent ran with LeastGrant installed and enforcement was observed happening.

How it attaches

A PreToolUse and PostToolUse command hook in settings.json. The payload arrives on stdin; the verdict goes to stdout as hookSpecificOutput.

Install it with leastgrant install. Configuration is written to ~/.claude/settings.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
allowyesprobehookSpecificOutput.permissionDecision
askdegradesprobeReaches a human when a human is there. In non-interactive mode (claude -p) a hook ask becomes a DENY. The README claimed an ask reaches you in every mode; probing showed otherwise.
denyyesprobeTwo channels: permissionDecision 'deny' and exit code 2. Honoured in the most permissive mode.

Interactive and unattended

This agent tells the hook which permission mode it is in — permission_mode, optional, top-level.

Modes it reports: default, plan, acceptEdits, auto, dontAsk, bypassPermissions. An ask reaches a person in default, plan, acceptEdits, auto, dontAsk, bypassPermissions. Interactively. Non-interactive (-p) turns any hook ask into a deny regardless of mode.

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 runs anyway. (probe) Measured: hook exits 1 with non-JSON stdout, the tool ran and permission_denials was empty.
  • On timeout — the call runs anyway, after 600s by default. Measured: timeout 2 against an 8s hook, the tool ran and the deny was discarded.
  • Can it be made to fail closed? — no. No per-hook fail-closed option.

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 commandsgatedprobe
file readsgatedprobe
file writesgatedprobe
deletionsgatedprobe
MCP callsgatedprobe
subagent spawngatedprobeThe model emits Task; the payload and matchers see Agent, because toolAliases resolves the name before hooks run.
network / webgatedsourceWebFetch/WebSearch are ordinary tools.

What decides this later

The highest-value write on your machine is not any single tool call. It is the file that settles what the agent may do next time — its hook configuration, its standing permission grants, the servers it is wired to, the memory a later session is handed as context. LeastGrant floors an agent write to every path below, so it always reaches a person, and test/control-files.test.ts reads this same list and fails the build if any of them stops being floored.

The list is longer than the install path on purpose. Walking it across every agent, rather than reading it, turned up three paths that were not floored at all — and the one that mattered most was Antigravity's config.json, which holds that host's own “Always allow” grants and outlives uninstalling LeastGrant entirely.

pathkindwhat it decides
~/.claude/settings.jsonhook configthe hook that makes LeastGrant run at all, plus permissions.allow
~/.claude/settings.local.jsonhook configthe same settings, per-machine, and they win
<repo>/.claude/settings.jsonhook configproject-scoped hooks and allow rules
~/.claude/commandsinstructionsslash commands a later session runs as if you had typed them
~/.claude/agentsinstructionssubagent definitions, including their system prompts
~/.claude/skillsinstructionsskills a later session loads and follows
<repo>/.mcp.jsonMCP wiringwhich MCP servers this project wires the agent to
managed-settings.jsonsettingsthe managed policy that can switch every hook off, including ours

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 — LeastGrant installed into a real Claude Code session; PreToolUse and PostToolUse observed firing, a credential read prompted, and a completed call recorded as evidence. 2.1.240 · win32 · 2026-09-02
  • Real transport probed — The hook is invoked over native stdin with the settings.json command string; exercised by every hook test in the suite driving the real binary. 2.1.240 · win32 · 2026-09-02
  • Contract read from the shipped build — Hook schema, permissionDecision values and mode list read from the shipped 337 MB bin/claude.exe bundle and cross-checked against code.claude.com/docs/en/hooks. 2.1.240 · win32 · 2026-09-02
  • Conformance suite — Driven by test/conformance.test.ts. 2.1.240 · 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.

  • 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.
  • 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.