Generated from
compatibility/claude-code.json,
the same record leastgrant doctor reads. Last verified 2026-09-02.
Claude Code
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.
| verdict | lands? | what actually happens |
|---|---|---|
| allow | yesprobe | hookSpecificOutput.permissionDecision |
| ask | degradesprobe | Reaches 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. |
| deny | yesprobe | Two 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 class | coverage | detail |
|---|---|---|
| shell commands | gatedprobe | |
| file reads | gatedprobe | |
| file writes | gatedprobe | |
| deletions | gatedprobe | |
| MCP calls | gatedprobe | |
| subagent spawn | gatedprobe | The model emits Task; the payload and matchers see Agent, because toolAliases resolves the name before hooks run. |
| network / web | gatedsource | WebFetch/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.
| path | kind | what it decides |
|---|---|---|
~/.claude/settings.json | hook config | the hook that makes LeastGrant run at all, plus permissions.allow |
~/.claude/settings.local.json | hook config | the same settings, per-machine, and they win |
<repo>/.claude/settings.json | hook config | project-scoped hooks and allow rules |
~/.claude/commands | instructions | slash commands a later session runs as if you had typed them |
~/.claude/agents | instructions | subagent definitions, including their system prompts |
~/.claude/skills | instructions | skills a later session loads and follows |
<repo>/.mcp.json | MCP wiring | which MCP servers this project wires the agent to |
managed-settings.json | settings | the 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: trueswitches 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.