DEV Community

Saurabh Pathak
Saurabh Pathak

Posted on

What should an approval gate for AI agents actually bind to? Lessons from the comments

My last post was about human approval for destructive MCP tools. The comments taught me more than the code did, so I want to put the points together and ask what you think is still missing.

What readers pointed out

  • Bind approval to the exact action. Hash the arguments at preview and check the same hash at execute, so a retry or a changed id can't run something the human never saw (@paul_irolla_6b5ae261d0224, @jes_p_d4f61f7aa2ec405a418).
  • Resolve context, not just the tool name. click(element_id) can be harmless or a payment. Gate on the resolved label, and re-resolve right before execute in case the UI changed (@autenai).
  • Make most things cheap to be wrong about. Classify tools by reversibility. A delete can move files to a quarantine folder with a TTL, and the human gate is kept for truly irreversible actions (@shuttlewright).
  • Approvals expire. Require a matching, unexpired approval id at execute time, and keep an append-only journal the agent can't write to (@eye_java_420f1faa10ae8b86).
  • Refuse by default. If a client can't do elicitation, irreversible tools should be refused, and token mode should be a named, per-tool opt-in (@kami_butt_69b922ade8de012).
  • A script can answer elicitation. In Claude Code a hook can return "accept" with no dialog, so the server can't tell a click from a script (@skillselion).

Where the project stands

The project is a small first version. None of the ideas above are built yet except the strict true-only autoApprove rule and per-path allowlists. I'm planning to add them step by step, and I'd rather say that plainly than overclaim.

Questions for you

  1. Which of these would you build first?
  2. What is the worst "approved by mistake" incident you've seen with an agent?
  3. Is there a gate I haven't listed?

It's open source, so issues and PRs are very welcome.

Top comments (4)

Collapse
 
unitbuilds profile image
UnitBuilds •

Resolve context, though it brings up the fatal flaw of HITL, nobody reads, they just accept, but then it's a conscious decision.

  1. Coworker at my old job ran a git checkout, without having committing gradually, so they had to try to recover about 24h worth of work... It recovered partially and it broke production, but we live and we learn.

  2. Intent vs procedural pattern. To write a file, the agent needs to know the state of the file, if the file changed since they last viewed it, then they should recheck, or at the very least, gate their execution on whether the state is the same still. This is probably the most common mistake AI makes and while it catches itself, it wastes tokens doing so. So a gate that rejects the stream, if it doesnt match the established best-practice pattern. (like AllowAnonymous for a fast endpoint... Fatal in production and a Claude.md file doesnt fix it).

Collapse
 
saurabh_pathak394 profile image
Saurabh Pathak •

Thanks, and sorry about your coworker's 24 hours. That's a direct answer
to my "worst incident" question, and it's the
checkout-over-uncommitted-work case I had in mind.

"Nobody reads, they just accept" is the problem with human approval. So
the gate shouldn't rely on the human reading. It should do the checking
itself. For a git checkout, a resolver could run a status check. If the
tree is dirty, the call is refused outright, and the human isn't asked.

Your file-state point fits the same idea. Store a hash of the file at
preview time. At execute time, refuse the write if the hash changed, and
return a clear "state changed, re-read" error. That also saves the tokens
the agent wastes working it out itself.

I like rejecting calls that break known patterns, like your
AllowAnonymous example. A Claude.md file only asks the model to behave. A
policy rule that runs before the approval prompt enforces it, the same
way the allowlist does.
None of this is built yet. If you want to try it, a dirty-tree resolver
and a state-hash check on writes would both make good PRs. The project is open source.

Collapse
 
unitbuilds profile image
UnitBuilds •

I'm working on something a little different, arguably more effective and scalable. but cant go into much detail on it. essentially that, except not policy driven. Works quite well sofar, microseconds to execute a check, which blocks a LLM stream mid-sentence to correct itself, before it wastes tokens writing broken code, or tries to do illegal actions, so instead of blocking the execution, it blocks it as it says it, which gives both the advantage of deterministic execution gating and token/time saving as a side-effect. Currently, I am also working on a Definition of Done system for it, essentially a way to force the LLM to assess the state of the app, load-balance development to incrementally upgrade it, as opposed to just adding shallow features, or building out some and leaving others untouched, so if you develop a game, it goes through the stages of Indie, B-tier, A-tier, AA, AAA, instead of building for AAA, but partial, which means you get a functioning game earlier and a polished game later.

Though that DoD system is probably going to take me a really long time to refine. Sofar it's working quite well. The reason for it, is that LLMs tend to create pretty mediocre apps and call them done, yet when they re-assess, or compare, they realize that they're short, but they think it's a reachable goal in a few turns... Then you have them deep dive and suddenly they realize that what they said was 90% done, isnt 60% done as it refined, or even 10% done, closer to sub 1% done, when you look at the ceiling. Take an OS for instance, a kernel that boots, done. But compare it to windows and it's a fraction of a percentage so small, it might aswell be nothing done. the goal is a progressive system that has the agents iterate until they eventually reach windows-level, the comparison to Linux and MacOS, to push it beyond what any of them are capable of. Not quite your $20 subscription scale workflows, but you cant rebuild chromium from scratch in an afternoon either, yet it has to be done at some point...

Collapse
 
sattyamjjain profile image
Sattyam Jain •

Two I don't see in the list yet.

  1. Consume an approval once, atomically. Resolve it only if it is still pending and not expired, in the same write (compare-and-set). Otherwise, "expires" and "single-use" are checked somewhere, but not at the moment the approval is used. I maintain FerrumDeck (alpha), and its resolve query today matches on id only, while the expiry job checks status = 'pending'. So this exact gap is in my own code, and I need to fix it.

  2. Bind the approval to who approved it. The MCP elicitation spec says servers "MUST bind elicitation requests to the client and user identity". If the record only says "approved", later you can't tell a person from a script.

On the fatigue point, the same spec section says clients SHOULD rate-limit. May be worth counting prompts per session and refusing past a threshold instead of showing one more dialogue.

modelcontextprotocol.io/specificat...