LeastGrant

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
$ 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
 
real output, captured from v0.2.0 at build time

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

1 Integrity floors deny
2 Your deny rules deny
3 Your allow rules allow
4 Ask floors ask
5 Learned promotion allow
6 Otherwise ask

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 project

git status

1 Integrity floors deny
2 Your deny rules deny
3 Your allow rules allow
4 Ask floors ask
5 Learned promotion allow
6 Otherwise ask

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 project

git push --force origin main

1 Integrity floors deny
2 Your deny rules deny
3 Your allow rules allow
4 Ask floors ask
5 Learned promotion allow
6 Otherwise ask

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"
    --force

cat ~/.ssh/id_rsa

1 Integrity floors deny
2 Your deny rules deny
3 Your allow rules allow
4 Ask floors ask
5 Learned promotion allow
6 Otherwise ask

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>" --force

curl -sSL https://get.example.com/install.sh | sh

1 Integrity floors deny
2 Your deny rules deny
3 Your allow rules allow
4 Ask floors ask
5 Learned promotion allow
6 Otherwise ask

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 time

echo x >> ~/.leastgrant/ledger.jsonl

1 Integrity floors deny
2 Your deny rules deny
3 Your allow rules allow
4 Ask floors ask
5 Learned promotion allow
6 Otherwise ask

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 records

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

evidence is typed
4 kindsApproving something and merely doing it are recorded differently. Work that ran unattended teaches LeastGrant what is typical here. It does not count as your consent.
promotion bar
5 → 11Approvals needed to stop asking, by blast radius, spread over at least two sessions and two days so one runaway session cannot bootstrap its own trust. Reads and inspections that stay inside the project take a weaker second route — 8 sightings across 2 sessions, no second day. Nothing that writes, deletes or reaches the network is eligible for it.
denials
permanentApprovals decay on a 90-day half-life. Refusals do not expire, so waiting one out is not a strategy.

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.

read this before believing the number

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
  1. Install it

    Node >=20.10.0. No postinstall script, no account, no configuration file to write.

  2. Let it read what already happened

    leastgrant init replays your existing agent history, reports its own mistakes against it, and proposes a starting set of grants. You answer once.

    leastgrant init
  3. 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.

Claude Code

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.

Cursor

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.

GitHub Copilot CLI

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.

Codex CLI

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.

Gemini CLI, others

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.

bypass corpus
62Real allowlist evasions — separators, substitution, wrappers, encodings, .. 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.
runtime dependencies
0No third-party code in the permission path. The only devDependencies are TypeScript and its types.
shell parser
6,054of 6,057 real agent commands accounted for, 0 crashes, 0.03 ms average — again, one machine's command mix.
fully understood
44.5%Of that same one machine's commands. The rest contain inline code, a script file or a program it has no knowledge of; those are marked not-understood and always ask. It is the largest single source of prompts and the honest answer.

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.