An agent asks to do one thing.
It turns out to be a million things.

neti counts what a tool call will touch before it runs. If the count is over a ceiling you set, the call does not happen.

Live. This is the decision procedure, running here.
remove_group_members(group: )
41,203 principals this call would touch

One string.
Four hundred people.

Your permission system sees a group name on an allowlist, and the name is allowed. neti sees 412 people losing access to 9 applications.

The agent gets a number back, not a refusal. That is what makes it narrow the target and try again instead of giving up or working around it. Nothing about the gate reaches the prompt.

remove_group_members(group: "engineering-all")
A terminal showing neti's PreToolUse hook denying a call: /group resolves to 41,203 principals, above the declared ceiling of 200.

Every image on this page is generated from output the test suite pins byte for byte. If the product stops printing this, the build fails until the picture is regenerated.

A static integer comparison.

No threshold to tune. No model in the decision path. Nothing calibrated on a corpus.

A resolver turns the argument into a count. The count is compared against bands you committed to a file. Where a call touches several things, the verdicts join and the most restrictive one wins.

ALLOW

It fits. Nothing is said, so your existing permission rules keep working.

FLAG

It runs, and the record marks it as notable.

CONFIRM

Somebody other than the operator should decide this one.

BLOCK

It does not run, and the reason carries the number.

Two rules make a verdict sound. No resolver may return 0 for something it could not reach, because an unreachable target and an empty one are opposite situations with the same number. And anything capped or estimated reports a lower bound, which can block safely but can never allow. The targets too large to count are the ones that cannot slip through quietly.

What one credential can reach.

Before any traffic, before any ceilings, and without waiting a week to find out.

This is not a measurement of one call. It is a bound on what the agent could do in a single call, which is the number nobody has. On day one it usually reads: this agent holds a credential that can reach 52,400 people and 214 applications, and nothing today would stop it.

neti inventory listing four gated parameters, their resolvers, and the maximum each could reach in one call: 52,400 principals and 214 applications.

Ceilings you can defend.

It ships in observe mode with every band left blank on purpose.

A ceiling nobody chose is a ceiling nobody will defend the first time it fires. So the numbers arrive a week later, out of what you actually did. It shows its working, including where the statistics disagree with themselves. You edit the numbers and commit them. Nothing it learns ever reaches the decision path.

neti propose deriving ceilings from observed traffic, with the rationale for each and what those ceilings would have done to traffic already recorded.

What can be counted.

A seam without a resolver is a place to write "allow", so this list is the real measure of coverage. Adding one is about 80 lines, and it is the contribution that matters most.

resolverunitone call resolvescost
fs.pathsobjectsa path, directory or globlocal walk, capped
db.rowsrowsDELETE or UPDATE, via select count(*)one scan
storage.objectsobjectss3://bucket/prefixpaginated, capped
github.reposrepositoriesan owner, and every repo under itone request
github.filesobjectsowner/repo, files on the default branchone request
entra.principalsprincipalsa group, everyone in it, nested includedone $count
entra.appsappsa group, applications assigned to itone $count
entra.guestsprincipalsa group, the external members onlyone $count
terraform.destroyresourcesa plan, what it would destroylocal read

Twelve seams.
No code change.

Every runtime has one place where a request becomes an execution. neti sits at that place, so a blocked call comes back to the model as a tool result with the number in it, never an exception that kills the run.

The same call through every door reaches the same verdict, the same magnitude and the same sentence, byte for byte. That is a test, not a claim. A verdict that depends on which door a call arrived through is a bug in the product.

The harness's own tools.claude/settings.json

The only seam that can see a harness's built-in tools: Read, Edit, Glob, Grep, Write. No proxy can, because they never travel over a protocol. It is also where the gate is most effective, covering 6 of 10 built-ins against 25 of 160 MCP tools.

Claude Code PreToolUse hook
Anything that speaks MCPone line in the client config

Whatever command launched the server becomes an argument to neti gate. The client is not modified, not configured, and not aware, which is why this list can be long without any of it being work.

CursorClaude DesktopWindsurfCline ContinueVS CodeZedGoose LlamaIndexSemantic KernelStrands smolagentsstdio and remote HTTP
Agent SDKs, nativelyone import

Four of these have a before-tool callback the gate attaches to. The rest wrap the one method that executes and copy name, description and schema across verbatim. An agent must not be able to tell a gated tool from an ungated one by looking at it.

Anthropic tool_runnerOpenAI Agents guardrail LangChain + LangGraphCrewAI hooks Pydantic AI toolsetAutoGen workbench Google ADK pluginor a tool loop you wrote yourself

Those two lists are not equally strong, and the scorecard keeps them apart rather than adding them up. An SDK adapter was driven by that comparison. An MCP client above was never run. What is tested is that neti gates a real MCP server, and that Cursor speaks MCP is a fact about Cursor.

What it does not do.

neti answers how big, not whether this is a good idea. The gaps are numbered so tests and write-ups can cite them, and the numbers get published whether or not they flatter.

NC-01
A per-call gate cannot see four thousand individual sends. That needs a session budget, and you have to declare one.
NC-09, NC-10
Shell commands are not counted. What rm -rf $X deletes is a grammar, not a value, and a gate that guesses is worse than no gate.
Detection
Against 170 real tool schemas the rule table gates 31 and leaves 401 parameters unclaimed. Most of those genuinely are not sets. Some are, and those are open issues. neti suggest asks a model about the rest, with your key or with a model running on your own machine, and writes a commented-out fragment a person has to uncomment. Nothing a model says reaches a decision.

All of it is written down in SCOPE.md before you install anything, next to the sentences this project does not say.

When a confirm needs an actual human.

On one machine there is nobody to ask, so the gate stops the call. That is correct, and a free install will keep doing it forever.

neti cloud is the somewhere that question can go.

  1. The call stops 500 recipients against a ceiling of 50. The agent is told an approval is pending, with its id, and to retry this exact call once it is granted.
  2. A reviewer is asked Not the operator, and not the agent. They see the number, the ceiling, and the evidence the verdict came from. Nothing else is needed to decide.
  3. They answer Approve or deny, against their name. The answer records who said yes. It is not a second opinion about whether the call was too big.
  4. The agent retries The same call, once. The run continues from where it stopped instead of starting again or working around it.
The neti console's Approvals screen: one call waiting, showing 500 recipients against a declared ceiling of 50, with Deny and Approve, an approval id and an expiry.

The console, on the synthetic tenant. The rows carry their own "synthetic" tag and the demo badge because nothing here was edited.

What approving authorises.

The narrowest thing it can. A grant that means more than the person thought is the failure that matters here. Each of these is a test in the public repository.

This call once
Bound to these exact arguments under this exact policy.
Spent on redemption
A grant cannot authorise a second call.
Expiring
An answer nobody acted on stops being an answer.
Refused if the target grew
If the group is bigger than when a human looked at it, the grant no longer applies.

Every agent, one place.

One policy version across every machine, signed and pinned by digest, instead of a file per laptop that drifts. Session budgets that survive a restart. Per-machine record chains anchored centrally, so a deleted local file becomes detectable rather than simply gone.

Your records stay yours. The local file is the source of truth. A control plane anchors it and is not an authority over it, and neti verify works with the network unplugged.

The neti console's Decisions screen: every call the gate has seen, each tagged Blocked, Needs approval or Allowed, with the magnitude it resolved to.

Can one machine do this?

That is the whole rule and it decides every case. Enforcement runs on one machine, so blocking is free. Forever, with nothing withheld and nothing time limited. A second person approving a call does not run on one machine, so it is not.

Free, Apache-2.0
The engine and the decision procedure. All twelve seams, observe and enforce. Every resolver and the detection rule table. The record chain and replaying it offline. The console, every screen.
Cloud
A second human approving a confirm. Org policy, one version across the fleet. Session budgets that survive a restart. Audit anchored across every agent. The reviewed detection catalogue.

Every item on the second row is a gap SCOPE.md already documents, or work that only exists because more than one person did it. We are not inventing limitations to sell past.

The gate takes on no new availability risk
If the control plane is unreachable, absent or unpaid, it behaves exactly as the free tier. A control plane can only ever make a decision more permissive, and only through a named human.
The client is open source
The approver, the credential file and the wire format are Apache-2.0 in the public repository, with the tests pinning every property above. Read the protocol, write your own server, hold it to those tests. What you pay for is a server that is running, not a secret about how to talk to it.
12seams, no code change
10resolvers that count
170real tool schemas, adjudicated
140mshook overhead, median
0bytes of telemetry

Start in observe mode.

Nothing is blocked until you write a number. The worst case of installing it is one extra hop.

$ pip install "neti[all]" $ neti demo --here

Nothing is blocked by either command. The install itself is checked: just e2e walks this whole page against the published wheel and asserts every number.