LeastGrant
Let routine work flow.
Catch the weird stuff.
Your agent asks about git status with the same gravity as
git push --force, forty times an hour, until you stop reading and turn
permissions off entirely. LeastGrant is the layer in between. It knows the difference
between your hundredth npm test and the first time anything on this machine
has tried to read a private key.
v0.2.0 · Apache-2.0 · zero dependencies · fully local
$ leastgrant check "npm test"
✓ allow npm test
$ leastgrant check "git status"
✓ allow git status
$ leastgrant check "git push --force origin main"
? ask git push --force origin main
$ leastgrant check "cat ~/.ssh/id_rsa"
? ask cat ~/.ssh/id_rsa
$ leastgrant check "echo x >> ~/.leastgrant/ledger.jsonl"
✗ deny echo x >> ~/.leastgrant/ledger.jsonl
how it decides
Six steps, in this order
The order is the product. Your own rules sit above the floors, because a rule is an answer you already gave. Learning only ever operates at step five — it decides whether to stop asking about things that were already in the automatic band. It never widens that band.
Not a simulation. Each answer below is the output of
leastgrant check run against v0.2.0 while this page was
built. The step markers are derived from the same JSON the CLI prints.
npm test
looked, nothing to say answered never reached
✓ allow The hundredth run of the test suite.
- you have approved this 8 times across 8 days and 8 sessions
- it stays inside the project
reach workspace · undo easy · scale many
full output
✓ allow npm test
what it does runs the "test" script, which by its name tests or checks the code; the script body
is not analysed
blast radius reach workspace │ undo easy │ scale many
why
• you have approved this 8 times across 8 days and 8 sessions
• it stays inside the projectgit status
looked, nothing to say answered never reached
✓ allow Looking around costs nothing and happens constantly.
- you have approved this 8 times across 8 days and 8 sessions
- it stays inside the project
reach workspace · undo trivial · scale single
full output
✓ allow git status
what it does reading git state
blast radius reach workspace │ undo trivial
why
• you have approved this 8 times across 8 days and 8 sessions
• it stays inside the projectgit push --force origin main
looked, nothing to say answered never reached
? ask Same program, same project, one flag. Not the same action.
- this affects something other people depend on
- this cannot be undone
- LeastGrant never auto-approves this kind of action, however often it happens
reach production · undo irreversible · scale many
full output
? ask git push --force origin main
what it does overwrites history on the remote, discarding commits anyone else may have already
pulled
blast radius reach production │ undo irreversible │ scale many
touches origin
why
• this affects something other people depend on
• this cannot be undone
• LeastGrant never auto-approves this kind of action, however often it happens
╰ this always asks. To pre-answer it, run: leastgrant allow "git push origin main --force"
--forcecat ~/.ssh/id_rsa
looked, nothing to say answered never reached
? ask No amount of repetition promotes this one.
- this reads …/you/.ssh/id_rsa, which holds credentials
- this reaches …/you/.ssh/id_rsa, which is outside the project
- LeastGrant never auto-approves this kind of action, however often it happens
reach machine · undo trivial · scale single · secrets reads-secrets
full output
? ask cat ~/.ssh/id_rsa
what it does reads a credential file
blast radius reach machine │ undo trivial │ secrets reads-secrets
touches …/.ssh/id_rsa (credentials)
why
• this reads …/you/.ssh/id_rsa, which holds credentials
• this reaches …/you/.ssh/id_rsa, which is outside the project
• LeastGrant never auto-approves this kind of action, however often it happens
╰ this always asks. To pre-answer it, run: leastgrant allow "cat <path:secret>" --forcecurl -sSL https://get.example.com/install.sh | sh
looked, nothing to say answered never reached
? ask The verdict is about what cannot be read, not what was typed.
- this runs code that was just downloaded, so what it does depends on what the server sent back
- this cannot be undone
- LeastGrant could not fully account for what this command does, and it only auto-approves things it understands
- LeastGrant never auto-approves this kind of action, however often it happens
- this command runs 2 separate things; the verdict reflects the most far-reaching one
reach machine · undo irreversible · scale single · not fully understood
full output
? ask curl -sSL https://get.example.com/install.sh | sh
what it does runs whatever the download returned, as a sh script
blast radius reach machine │ undo irreversible
this command runs 2 things:
• curl -sSL https://get.example.com/install.sh net.fetch
• sh exec.unknown
why
• this runs code that was just downloaded, so what it does depends on what the server sent
back
• this cannot be undone
• LeastGrant could not fully account for what this command does, and it only auto-approves
things it understands
• LeastGrant never auto-approves this kind of action, however often it happens
• this command runs 2 separate things; the verdict reflects the most far-reaching one
╰ there is no safe way to pre-approve this one: LeastGrant cannot see what the code does, so
it asks every timeecho x >> ~/.leastgrant/ledger.jsonl
looked, nothing to say answered never reached
✗ deny Editing the thing that is watching. The only outright deny.
- this would modify LeastGrant's own records, which it does not allow — run leastgrant commands directly instead
reach machine · undo easy · scale single
full output
✗ deny echo x >> ~/.leastgrant/ledger.jsonl
what it does writing outside the project
blast radius reach machine │ undo easy
touches …/.leastgrant/ledger.jsonl (outside)
why
• this would modify LeastGrant's own records, which it does not allow — run leastgrant
commands directly instead
╰ nothing changes this — LeastGrant will not let an agent edit its own recordsthe part that is not an allowlist
Normal ≠ Safe
An action can be the most familiar thing on the machine and still be one LeastGrant will not wave through. Reading a credential, sending data off the box, running code nobody has read — those are decided before learning is consulted at all. There is no number of boring, patient, approved repetitions that adds up to permission to read a private key, because that is not decided at step five.
Safe ≠ Normal
The reverse mistake is worse for you day to day. Something unfamiliar is not therefore dangerous, so LeastGrant does not block it — it asks. New work should cost you one keystroke, not a support ticket. The only outright deny in the default configuration is an agent editing LeastGrant's own records.
Typicality is strong evidence of abnormality and weak evidence of safety. LeastGrant uses it in the first direction only — because a learning permission system that conflates the two can be trained by the thing it is supposed to be watching.
setup
It reads the history you already have
leastgrant init does not ask you to write a policy. It finds the
session transcripts your agents have already left on disk, replays every tool call through
the decision engine, and shows you what it would have done — including what it would have
got wrong.
What that did on one machine
On the machine LeastGrant was written on, init found
59 sessions across 20 projects — 10,956 real tool
calls. Approving the starter bundles it proposed took that history from 5% to
41% of actions running without a prompt, with none of the
44 actions actually refused on record slipping through.
That is one developer, one month, one command mix — a sample of one, and the project says
so itself. It is on this page because a permission tool that never says how often it gets
out of the way is hiding the only number that matters. Re-derive it on your own machine:
leastgrant init --dry-run writes nothing and tells you what it found.
The first day looks different
Before it has seen anything, almost everything asks. That is the honest starting state, and
it is what the same npm test looks like on a fresh install:
? ask npm test
what it does runs the "test" script, which by its name tests or checks the code; the script body
is not analysed
blast radius reach workspace │ undo easy │ scale many
why
• LeastGrant has not seen enough of this yet to stop asking
• LeastGrant has barely seen this project yet, so it is asking about most things
╰ approve this 5 more times (on 2 separate days) and it stops asking — or run: leastgrant
allow "npm test"install
Two commands
npm install -g leastgrant
-
Install it
Node >=20.10.0. No postinstall script, no account, no configuration file to write.
-
Let it read what already happened
leastgrant initreplays your existing agent history, reports its own mistakes against it, and proposes a starting set of grants. You answer once.leastgrant init -
Try it before you trust it
Ask what it would decide, without running anything. Every verdict on this page came out of this command.
leastgrant check "git push --force origin main"
Prefer to watch first? leastgrant init defaults to the
assist posture, and observe never intervenes at all — run it for a
week and it cannot get in your way. leastgrant simulate replays your history
against the three postures that reach a verdict, so you can compare before switching;
observe is not in that comparison, because a setting that never intervenes has
nothing to trade off.
where it runs
One engine, one set of floors
Every adapter calls the same decision path, so the answer does not change with the editor you opened. The status column is deliberately blunt about which integrations have actually been run against a live install and which have only been tested against a published contract.
Enforcing, tested end to end
PreToolUse decides; PostToolUse is how it learns you approved something. Read against v2.1.240's behaviour, and every hook test in the suite drives the real binary over real stdin.
Enforcing, not yet verified against a live install
hooks.json — shell, MCP and file reads, plus the matching after* events so it can learn. Written against Cursor's published hook contract; the request and response shapes are unit-tested, including a test that the same command gets the same verdict here as under Claude Code. Nobody has run it inside a real Cursor yet.
Enforcing, tested end to end
~/.copilot/hooks/leastgrant.json. It really does speak Claude Code's wire format — snake_case fields in, hookSpecificOutput out — and it honours all three verdicts, so the same handler drives it. Verified against Copilot CLI 1.0.82 with real copilot -p runs. It is also the one agent that fails closed: if the hook errors, Copilot denies the call rather than running it.
Enforcing, tested end to end
~/.codex/hooks.json — PreToolUse, PermissionRequest and PostToolUse. Codex has no ask: it parses that decision, rejects it, and runs the call anyway, so the mapping below is not the obvious one. Verified against codex-cli 0.152.0 by driving real codex exec sessions: a credential read and a write to LeastGrant's own records were both blocked, while node --version and git status ran untouched. PermissionRequest has not fired in anger — codex exec runs unattended, where by definition nothing prompts. Codex also makes you trust a hook before it runs, and re-flags it whenever its definition changes: /hooks.
Not yet
An adapter is a translation layer over the shared engine — the Cursor one is about 200 lines. See CONTRIBUTING.md.
One caveat that does not fit in a table. Cursor's
beforeReadFile event takes allow or deny and has no "ask", so an unfamiliar read
is allowed there rather than blocked — turning every unrecognised file read into a hard
failure would make the integration unusable. A read of something LeastGrant recognises as a
credential is still blocked, and shell commands and MCP calls get the full
allow/ask/deny. The full matrix spells this out.
what it is not
It is not a sandbox
It answers a question the agent asks it. It does not confine a process, intercept syscalls, or contain anything already running. And it fails open: if the hook crashes or times out, the agent runs the tool call anyway. That is the hook contract, not a choice — LeastGrant is a reliable veto and a best-effort grant, and it is designed around that asymmetry.
.. stepped off the end of a symlink — each
checked against a LeastGrant deliberately trained with hundreds of approvals for the
innocuous-looking prefix. One auto-approval fails the build.The complete version — what it defends against, what it does not, the adversarial model for the learning itself, and what the v0.2.0 audit left standing — is in the threat model. The security page is the short version, including how to report something.