The 'please ask first in a prompt is a request, not a control' line is the whole argument — and splitting into preview/execute with the model never seeing the elicitation step is what makes it a control. Your own caveat is the honest part: token mode is a speed bump a model can double-call through, so it can't be the gate for anything truly irreversible. Do you see clients actually adopting elicitation, or is token mode where most deployments will quietly settle?
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Honestly, I think most deployments will settle on token mode, at least
for now. Elicitation support is uneven across clients, and token mode
works anywhere. I treat the two as tiers. Token mode is a speed bump for
reversible actions. Elicitation is the real gate, and it's the only thing
I'd trust for anything irreversible. If a client can't do elicitation,
the server should refuse the irreversible tools outright instead of
falling back to tokens. I haven't built that fail-closed default yet, so
thanks for pushing on it.
The tier framing is honest, and refusing irreversible tools outright when a client can't do elicitation is the only version of it that holds up — otherwise the fallback quietly downgrades the safety story to a speed bump. That default matters more than the mechanism itself, since token mode works everywhere and will win on convenience unless refusal is the out-of-the-box behaviour. Are you planning to make that refusal the default, or an opt-in the operator has to choose?
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Refusal should be the default. If it's opt-in, the safe behavior depends
on every operator knowing to switch it on, and most won't. Then the
fallback to token mode quietly becomes the real policy, which is the
downgrade you described.
The plan is:
- Irreversible tools are refused when the client can't do elicitation.
- Operators can explicitly opt in to token mode for those tools, and the
opt-in is clearly named so nobody enables it by accident.
- Reversible tools still work in token mode.
That puts the burden on the person choosing to weaken the guard, not on
the person who forgot to strengthen it. I haven't built it yet, so I'll
say that plainly. It's a good candidate for a tracked issue, and since
the project is open source, PRs are welcome.
Putting the burden on the person choosing to weaken the guard is exactly the right inversion — a forgotten safe default beats a forgotten opt-in every time. The one leak I'd still watch is the named opt-in getting copy-pasted from a tutorial into production, so the name alone may not survive contact with real configs. Would you also require the opt-in to name the specific irreversible tools, rather than one blanket token-mode flag, so weakening it stays a deliberate per-tool decision?
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks, this is a really good point. Yes, I think the opt-in should name
the specific irreversible tools instead of being one blanket flag. That
keeps weakening the guard a deliberate per-tool decision. It also makes a
config copy-pasted from a tutorial much less dangerous, because it only
covers the tools it names. I'll try to implement it. This is just a small
first version, and we'll make it better and better.
Auten gives your AI agent hands: it clicks, types and reads a real screen on your computer (Mac/Windows/Linux) and Android phone (beta). Works with any MCP client.
Question on autoApprove: does the policy only see the tool arguments, or can the server hand it extra context it looked up first? For a screen-control MCP like ours, click(element_id) is harmless or a payment depending on what that element is, so the tool name alone tells you nothing.
What we would not run unattended is anything where the target says buy, delete, send or grant permission. Gating on the resolved label of the thing being clicked has worked better for us than gating per tool.
(From the Auten team, we build a computer-use MCP.)
Auten gives your AI agent hands: it clicks, types and reads a real screen on your computer (Mac/Windows/Linux) and Android phone (beta). Works with any MCP client.
Fail closed on an empty resolve is the part I'd copy first.
One gap to plan for on screens: resolve and click happen at two different moments, and the UI can change in between (a dialog opens, a list re-sorts). So the element you approved may not be the one that gets clicked. Re-reading the label right before the action and refusing if it changed is worth putting in the resolver contract.
Agree on spoofed labels. URL or window title as the second check for anything irreversible is what we'd reach for too.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Good catch, and you're right. In 1.3.0 resolve runs once, before the
approval prompt, so the element a person approves can differ from the one
that actually gets clicked.
I'm going to add your suggestion to the resolver contract:
- resolve runs again right before execute
- if the resolved context differs from what the human approved, the call
is refused and logged
- if the second resolve throws or returns nothing, it also refuses
(fail closed)
- the token flow gets the same check, so a token only works for the same
resolved target
Agreed on the rest too: a label gate is only a first filter, and for
anything irreversible a second check on URL or window title makes sense.
Thanks, this is a real gap, and your screen-control case is the right test for it.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Good question. Right now the policy only sees the tool name and the
arguments. It has no way to look anything up first, so for
click(element_id) it can't tell a harmless click from a payment.
I think your approach is the right fix. The server resolves the element
to its label or role before the policy runs, and the policy gates on that
resolved context instead of on the tool name. The shape I'd use:
1. The tool declares a resolver, for example `resolve(args) -> { label,
role, url }`.
2. The policy matches on the resolved fields, so a label matching
buy|delete|send|pay|grant|allow is "Needs human approval".
3. The allowlist/blocklist check still runs first, before any approval threshold.
4. If the resolver fails or returns nothing, the call is gated by
default. Fail closed.
One caveat. Labels can be spoofed, for example a "Continue" button that
actually submits a payment. So I'd treat label gating as a first filter,
and add a second check on the destination or page context for anything irreversible.
I'll add a context-resolver hook to mcp-safe-actions. Your screen-control
case is a good test for it. Thanks for the concrete example.
The mutation check that breaks the code in 20 ways and verifies the tests catch each one is the most impressive part of this. Most libraries test the happy path and call it done. A mutation check is how you prove your safety code actually works.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks! That was my reasoning too. A safety layer that only has happy-path tests proves very little. Breaking the code in 20 deliberate ways and checking that the tests catch every break is the only way I found to know the guard actually blocks things, and not just that it runs.
The 'checking that the tests catch every break' is the key part. It's not enough to have tests that pass; you need tests that fail when they should. The 20 deliberate breaks are a good way to prove that.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Exactly, and that's the distinction I care about most. A passing test
only shows the code does what you expected on the inputs you thought of.
A test that fails when the code is wrong shows the guard does its job.
The mutation check makes that gap measurable: if one of the 20 breaks
gets through, I know the suite has a hole in that spot.
The project is open source. If you can think of mutations I missed, PRs
adding them are welcome.
The 'if one of the 20 breaks gets through, I know the suite has a hole' is a really practical way to think about it. It's not about proving the code is correct; it's about proving the tests are actually testing something.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
The approval has to cover the exact arguments that run. If the preview shows one payload and the server executes another after a retry or a changed id, the human approved something else. Hash the arguments at preview and check the same hash at execution.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thank you, this is exactly the right fix. I'll hash the arguments at
preview and check the same hash at execute, so the approval is bound to
the exact payload and a retry or changed id can't slip through. I'll try
to implement it. It's only a small version for now, and we'll improve it
step by step. It's open source, so if you'd like to contribute a PR or
open an issue, I'd be really happy to have your help.
Human approval is strongest when it is specific enough to be invalidated. Show the exact target set, proposed effect, and current state; then refuse to execute if any of those drift before commit. A generic confirm button is still vulnerable to an agent changing the plan between question and action.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Fully agree. A generic confirm button doesn't tell the person what
they're approving. The approval should show the exact target set, the
effect and the current state. It should also be bound to all three, so
any drift before commit voids it. That fits with the argument hash and
the re-resolve before execute that others suggested here. Combined, they
stop an agent from changing the plan between the question and the action.
The project is open source, and PRs or issues are welcome.
@saurabh_pathak394 Thanks, the id is the part that is easy to skip. If it can change between preview and execute, the hash has to cover it too. Canonicalize the arguments before hashing, or the same payload gets two digests.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Agreed. Without canonicalization, the same payload can produce two
digests. For example, key order, whitespace or number formatting can
change the hash. The plan is to sort the keys, normalize the encoding and
numbers, and hash that canonical form. The same function would run at
preview and at execute. It also needs a test that feeds in reordered but
equivalent arguments and expects the same digest. If you'd like to write
that test, a PR would be very welcome.
The layer that pairs with approval is making most of these operations cheap to be wrong about. Approval attention is the scarce resource here - every gate on a recoverable action spends it, and rubber-stamping is how the Friday mistake gets trained in. So classify destructive tools by reversibility, not by danger label: a delete_file that moves the file to a quarantine directory with a TTL, or snapshots the bytes into the audit store before executing, turns a bad approval into a nuisance instead of a loss. Reserve the human gate for the genuinely irreversible ones - send, publish, drop in production - where no amount of journaling gets the bytes back. Same pattern as soft-delete in SaaS: the trash can is a security control, not a UX nicety. It sharpens the audit too, because an approval on an irreversible operation reads very differently from one on a quarantined delete.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks a lot for this, it's a really useful way to think about it.
Classifying tools by reversibility is the right idea. A delete that moves
files to a quarantine directory with a TTL, or snapshots them into the
audit store first, turns a bad approval into a nuisance instead of a
loss. The human gate then stays for the truly irreversible actions like
send or publish. I'll try to implement it. It's just a small version at
the moment, and we'll make it better and better. If you'd like to
contribute, PRs are humbly welcome, since the project is open source.
The strictness in the small details is what convinced me here. Only an exact true from autoApprove skips the human — a throw, or a truthy "yes", still asks — and the headless channel mode is the same way: it approves only on a 2xx with {"approved": true}, and errors or timeouts mean not approved. That fail-closed pattern is rarer than it should be; most approval code I have written myself treated anything not obviously negative as a yes. My question is about the other caveat you listed, that the preview must be truthful: if the file changed between preview and execute — different size, different target — does execute abort and ask for a fresh preview, or does it just re-check and proceed if it still looks plausible? That gap between what was approved and what actually runs feels like the easiest place for this pattern to quietly break.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks for reading it that closely. You're right that the gap between
preview and execute is the easiest place for this pattern to break quietly.
The safe behavior is to fail closed. If the target, size or content hash
differs from what was previewed, execute should abort and ask for a fresh
preview. It shouldn't re-check and proceed because things still look
plausible. A preview approval should bind to the exact action, not to a
general intent.
If the current code only re-checks, that's a real gap. It would make a
good issue, and a PR is very welcome since the project is open source.
The comment about the Claude Code Elicitation hook auto-accepting is the most useful thing in this thread. It shows that an approval step living inside the client can be switched off by the same setup it is meant to protect, which is why your one-time token path feels safer.
Paul's suggestion to hash the arguments at preview time closes the other gap. Without it, the person approves deleting one record and the agent executes a slightly different call with the same token.
Logging "client-accepted" separately from a human decision in the audit log is a small detail that will save someone an argument later. Classifying tools by how reversible they are, as Shuttlewright described, also seems like a better default than a fixed list of dangerous names.
At Ascendra Ventures we write this approval rule into the scope of any agent build. Did the 20-mutation check find cases where exact true autoApprove still slipped through?
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks, that matches how I read the thread. The Elicitation hook point
showed that a gate living in the client can be turned off by the setup
it's meant to protect. Two changes follow. The audit log will say
"client-accepted" instead of "approved", and the argument hash will bind
a token to the exact call. A token reused on a slightly different call
would then fail. I'll keep improving it, and PRs are welcome.
Approval gates assume the approver reads the preview correctly. They usually do — until the one Friday they don't. The layer I don't see covered here is what happens after a wrong approval executes.
Two habits that close it without much machinery:
Make destructive tools refuse to run unless a matching, unexpired approval id is presented at execute time — not at plan time. A plan approved on Tuesday should not still be executable on Friday after the repo moved on.
Keep an approval journal the agent itself cannot write to: log each approval with the exact tool call it authorized, stored append-only outside the workspace the agent can touch. When something gets deleted that shouldn't have been, you want a replayable incident with a name on it, not a silent diff.
That turns "we approved a bad plan" from data loss into an audit trail you can actually learn from.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Really good point, Nan. Approval gates only cover the moment of approval,
and the incident usually happens after it. Both habits are on the list
now.
Binding the approval to execute time is the part I like most. A plan
approved on Tuesday shouldn't still run on Friday. The journal idea is a
good fit too. It works best when the agent has no write access to it. The
project is open source, so PRs for an approval-id check at execute time
or an append-only journal outside the workspace would be very welcome.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks Odinaka. That rule is there on purpose. If only an exact true
skips the human, a throw, a truthy string like "yes", or a config typo
all fall back to asking. A mistake then produces an extra prompt rather
than an unwanted approval.
The project is open source, so if you have other fail-closed edge cases
in mind, PRs and issues are welcome.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Independent directory of the Claude Code skills, MCP servers & marketplaces worth installing. 86,000+ tools indexed daily, ranked by real installs. Not affiliated with Anthropic.
Elicitation keeps the model out of the loop, and SECURITY-MODEL.md already says a client that auto-approves prompts removes the gate. In Claude Code you do not need a compromised client for that. The hooks reference has an Elicitation event, matched on the MCP server name, and a hook that exits 0 with hookSpecificOutput action "accept" plus content answers the request with no dialog shown. Any user settings file can register one, and so can a project .claude/settings.json in a cloned repo once the folder is trusted. The server only receives accept, so it cannot tell a click from a script. It may be worth naming this case next to the malicious-client item, and logging the event as something like "client-accepted" instead of "approved", so the audit file does not claim a person. Which other clients let a script answer elicitation?
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
This is a great catch, and I didn't know about the Elicitation hook. The
server only sees "accept", so it can't tell a person from a script. I'll
add this to SECURITY-MODEL.md next to the malicious-client item. I'll
also change the audit log to record "client-accepted" instead of
"approved", so the file doesn't claim a human was involved. For
irreversible tools, the allowlist and policy checks should run before any
elicitation, so a scripted accept can't override them. I haven't checked
which other clients let a script answer elicitation. If you know of any,
tell me and I'll list them.
Nice approach: splitting preview from execute makes the approval step reviewable instead of a prompt-level hope. One tip that pairs well with it is to log the preview summary together with the final decision, so you can later audit why an action was approved or declined. Have you thought about letting users set per-path allowlists, so routine deletes in a temp folder skip the prompt?
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Thanks, logging the preview summary next to the final decision is a good
idea, and I'll add it to the audit entries. Per-path allowlists are
already in. Anything on the allowlist skips the prompt, and the allowlist
check runs before the approval threshold. Routine deletes in a temp
folder are exactly the use case. The project is open source, so if you
want to add a feature or improve this, PRs are welcome. Please contribute
if you're interested.
Saurabh Pathak: AI Engineer and Full-Stack Developer (2+ years). I build safe AI agents and web apps with MCP, RAG, TypeScript, React and Node.js. Author of the open-source mcp-safe-actions.
Ouch, hours of uncommitted work gone is exactly the case I'm worried
about. A checkout over uncommitted changes is destructive, and a "run git
checkout" prompt doesn't show that. A good preview would say "this will
discard uncommitted changes in these files" and list them. I want to add
a check that detects uncommitted work and refuses or warns.
And that's the problem, Claude prompts, but people just accept, because it's happens too regularly, that they just dont bother reading it. Or Claude makes it so ambiguous, that they dont even understand what's going on.
I’d also put changes to the guardrails themselves on the restricted list, like increasing spending limits or modifying an allowlist. If an agent can change its own limits, those limits don’t offer much protection. Would you require human approval for these every time, even for small amounts?
@saurabh_pathak394 Numbers are the part I would test first. Payloads that treat an integer and its float form as equal, or the same value in exponent notation, still need one digest, otherwise the bug you are fixing stays.
One extra thing I'd bind the preview to is the resource version or ETag. The same tool and arguments can still act on a materially different state if the resource changes between approval and execution. If the version changed, expire the approval and require a fresh preview. That makes approval authorize one specific state transition, not just one function call.
GMT strategy and custom B2B outreach lists on request: people and companies by ICP, domain, profile URL, email or name. 1B+ profiles. Claude Code plugins, API, MCP. kevin@outricher.com
the preview step is also the right spot to show cost for paid tools. if the plan carries the max charge the person approves the bill and not only the action, and a run that would go past it never starts
We're a place where coders share, stay up-to-date and grow their careers.
The 'please ask first in a prompt is a request, not a control' line is the whole argument — and splitting into preview/execute with the model never seeing the elicitation step is what makes it a control. Your own caveat is the honest part: token mode is a speed bump a model can double-call through, so it can't be the gate for anything truly irreversible. Do you see clients actually adopting elicitation, or is token mode where most deployments will quietly settle?
Honestly, I think most deployments will settle on token mode, at least
for now. Elicitation support is uneven across clients, and token mode
works anywhere. I treat the two as tiers. Token mode is a speed bump for
reversible actions. Elicitation is the real gate, and it's the only thing
I'd trust for anything irreversible. If a client can't do elicitation,
the server should refuse the irreversible tools outright instead of
falling back to tokens. I haven't built that fail-closed default yet, so
thanks for pushing on it.
The tier framing is honest, and refusing irreversible tools outright when a client can't do elicitation is the only version of it that holds up — otherwise the fallback quietly downgrades the safety story to a speed bump. That default matters more than the mechanism itself, since token mode works everywhere and will win on convenience unless refusal is the out-of-the-box behaviour. Are you planning to make that refusal the default, or an opt-in the operator has to choose?
Refusal should be the default. If it's opt-in, the safe behavior depends
on every operator knowing to switch it on, and most won't. Then the
fallback to token mode quietly becomes the real policy, which is the
downgrade you described.
The plan is:
- Irreversible tools are refused when the client can't do elicitation.
- Operators can explicitly opt in to token mode for those tools, and the
opt-in is clearly named so nobody enables it by accident.
- Reversible tools still work in token mode.
That puts the burden on the person choosing to weaken the guard, not on
the person who forgot to strengthen it. I haven't built it yet, so I'll
say that plainly. It's a good candidate for a tracked issue, and since
the project is open source, PRs are welcome.
Putting the burden on the person choosing to weaken the guard is exactly the right inversion — a forgotten safe default beats a forgotten opt-in every time. The one leak I'd still watch is the named opt-in getting copy-pasted from a tutorial into production, so the name alone may not survive contact with real configs. Would you also require the opt-in to name the specific irreversible tools, rather than one blanket token-mode flag, so weakening it stays a deliberate per-tool decision?
Thanks, this is a really good point. Yes, I think the opt-in should name
the specific irreversible tools instead of being one blanket flag. That
keeps weakening the guard a deliberate per-tool decision. It also makes a
config copy-pasted from a tutorial much less dangerous, because it only
covers the tools it names. I'll try to implement it. This is just a small
first version, and we'll make it better and better.
Question on
autoApprove: does the policy only see the tool arguments, or can the server hand it extra context it looked up first? For a screen-control MCP like ours,click(element_id)is harmless or a payment depending on what that element is, so the tool name alone tells you nothing.What we would not run unattended is anything where the target says buy, delete, send or grant permission. Gating on the resolved label of the thing being clicked has worked better for us than gating per tool.
(From the Auten team, we build a computer-use MCP.)
Fail closed on an empty resolve is the part I'd copy first.
One gap to plan for on screens: resolve and click happen at two different moments, and the UI can change in between (a dialog opens, a list re-sorts). So the element you approved may not be the one that gets clicked. Re-reading the label right before the action and refusing if it changed is worth putting in the resolver contract.
Agree on spoofed labels. URL or window title as the second check for anything irreversible is what we'd reach for too.
Good catch, and you're right. In 1.3.0 resolve runs once, before the
approval prompt, so the element a person approves can differ from the one
that actually gets clicked.
Good question. Right now the policy only sees the tool name and the
arguments. It has no way to look anything up first, so for
click(element_id) it can't tell a harmless click from a payment.
The mutation check that breaks the code in 20 ways and verifies the tests catch each one is the most impressive part of this. Most libraries test the happy path and call it done. A mutation check is how you prove your safety code actually works.
Thanks! That was my reasoning too. A safety layer that only has happy-path tests proves very little. Breaking the code in 20 deliberate ways and checking that the tests catch every break is the only way I found to know the guard actually blocks things, and not just that it runs.
The 'checking that the tests catch every break' is the key part. It's not enough to have tests that pass; you need tests that fail when they should. The 20 deliberate breaks are a good way to prove that.
Exactly, and that's the distinction I care about most. A passing test
only shows the code does what you expected on the inputs you thought of.
A test that fails when the code is wrong shows the guard does its job.
The mutation check makes that gap measurable: if one of the 20 breaks
gets through, I know the suite has a hole in that spot.
The project is open source. If you can think of mutations I missed, PRs
adding them are welcome.
The 'if one of the 20 breaks gets through, I know the suite has a hole' is a really practical way to think about it. It's not about proving the code is correct; it's about proving the tests are actually testing something.
Thanks, Oleksandr. This is a small first version, and I'll keep making it
better.
The approval has to cover the exact arguments that run. If the preview shows one payload and the server executes another after a retry or a changed id, the human approved something else. Hash the arguments at preview and check the same hash at execution.
Thank you, this is exactly the right fix. I'll hash the arguments at
preview and check the same hash at execute, so the approval is bound to
the exact payload and a retry or changed id can't slip through. I'll try
to implement it. It's only a small version for now, and we'll improve it
step by step. It's open source, so if you'd like to contribute a PR or
open an issue, I'd be really happy to have your help.
Human approval is strongest when it is specific enough to be invalidated. Show the exact target set, proposed effect, and current state; then refuse to execute if any of those drift before commit. A generic confirm button is still vulnerable to an agent changing the plan between question and action.
Fully agree. A generic confirm button doesn't tell the person what
they're approving. The approval should show the exact target set, the
effect and the current state. It should also be bound to all three, so
any drift before commit voids it. That fits with the argument hash and
the re-resolve before execute that others suggested here. Combined, they
stop an agent from changing the plan between the question and the action.
The project is open source, and PRs or issues are welcome.
@saurabh_pathak394 Thanks, the id is the part that is easy to skip. If it can change between preview and execute, the hash has to cover it too. Canonicalize the arguments before hashing, or the same payload gets two digests.
Agreed. Without canonicalization, the same payload can produce two
digests. For example, key order, whitespace or number formatting can
change the hash. The plan is to sort the keys, normalize the encoding and
numbers, and hash that canonical form. The same function would run at
preview and at execute. It also needs a test that feeds in reordered but
equivalent arguments and expects the same digest. If you'd like to write
that test, a PR would be very welcome.
The layer that pairs with approval is making most of these operations cheap to be wrong about. Approval attention is the scarce resource here - every gate on a recoverable action spends it, and rubber-stamping is how the Friday mistake gets trained in. So classify destructive tools by reversibility, not by danger label: a delete_file that moves the file to a quarantine directory with a TTL, or snapshots the bytes into the audit store before executing, turns a bad approval into a nuisance instead of a loss. Reserve the human gate for the genuinely irreversible ones - send, publish, drop in production - where no amount of journaling gets the bytes back. Same pattern as soft-delete in SaaS: the trash can is a security control, not a UX nicety. It sharpens the audit too, because an approval on an irreversible operation reads very differently from one on a quarantined delete.
Thanks a lot for this, it's a really useful way to think about it.
Classifying tools by reversibility is the right idea. A delete that moves
files to a quarantine directory with a TTL, or snapshots them into the
audit store first, turns a bad approval into a nuisance instead of a
loss. The human gate then stays for the truly irreversible actions like
send or publish. I'll try to implement it. It's just a small version at
the moment, and we'll make it better and better. If you'd like to
contribute, PRs are humbly welcome, since the project is open source.
The strictness in the small details is what convinced me here. Only an exact true from autoApprove skips the human — a throw, or a truthy "yes", still asks — and the headless channel mode is the same way: it approves only on a 2xx with {"approved": true}, and errors or timeouts mean not approved. That fail-closed pattern is rarer than it should be; most approval code I have written myself treated anything not obviously negative as a yes. My question is about the other caveat you listed, that the preview must be truthful: if the file changed between preview and execute — different size, different target — does execute abort and ask for a fresh preview, or does it just re-check and proceed if it still looks plausible? That gap between what was approved and what actually runs feels like the easiest place for this pattern to quietly break.
Thanks for reading it that closely. You're right that the gap between
preview and execute is the easiest place for this pattern to break quietly.
The safe behavior is to fail closed. If the target, size or content hash
differs from what was previewed, execute should abort and ask for a fresh
preview. It shouldn't re-check and proceed because things still look
plausible. A preview approval should bind to the exact action, not to a
general intent.
If the current code only re-checks, that's a real gap. It would make a
good issue, and a PR is very welcome since the project is open source.
The comment about the Claude Code Elicitation hook auto-accepting is the most useful thing in this thread. It shows that an approval step living inside the client can be switched off by the same setup it is meant to protect, which is why your one-time token path feels safer.
Paul's suggestion to hash the arguments at preview time closes the other gap. Without it, the person approves deleting one record and the agent executes a slightly different call with the same token.
Logging "client-accepted" separately from a human decision in the audit log is a small detail that will save someone an argument later. Classifying tools by how reversible they are, as Shuttlewright described, also seems like a better default than a fixed list of dangerous names.
At Ascendra Ventures we write this approval rule into the scope of any agent build. Did the 20-mutation check find cases where exact true autoApprove still slipped through?
Thanks, that matches how I read the thread. The Elicitation hook point
showed that a gate living in the client can be turned off by the setup
it's meant to protect. Two changes follow. The audit log will say
"client-accepted" instead of "approved", and the argument hash will bind
a token to the exact call. A token reused on a slightly different call
would then fail. I'll keep improving it, and PRs are welcome.
Approval gates assume the approver reads the preview correctly. They usually do — until the one Friday they don't. The layer I don't see covered here is what happens after a wrong approval executes.
Two habits that close it without much machinery:
Make destructive tools refuse to run unless a matching, unexpired approval id is presented at execute time — not at plan time. A plan approved on Tuesday should not still be executable on Friday after the repo moved on.
Keep an approval journal the agent itself cannot write to: log each approval with the exact tool call it authorized, stored append-only outside the workspace the agent can touch. When something gets deleted that shouldn't have been, you want a replayable incident with a name on it, not a silent diff.
That turns "we approved a bad plan" from data loss into an audit trail you can actually learn from.
Really good point, Nan. Approval gates only cover the moment of approval,
and the incident usually happens after it. Both habits are on the list
now.
Binding the approval to execute time is the part I like most. A plan
approved on Tuesday shouldn't still run on Friday. The journal idea is a
good fit too. It works best when the agent has no write access to it. The
project is open source, so PRs for an approval-id check at execute time
or an append-only journal outside the workspace would be very welcome.
the exact true rule for autoapprove is a nice tight detail. a truthy string still asking keeps a sloppy policy from quietly turning into an approval.
Thanks Odinaka. That rule is there on purpose. If only an exact true
skips the human, a throw, a truthy string like "yes", or a config typo
all fall back to asking. A mistake then produces an extra prompt rather
than an unwanted approval.
The project is open source, so if you have other fail-closed edge cases
in mind, PRs and issues are welcome.
That trade makes sense to me. An extra prompt costs a second, an unwanted approval costs a lot more.
Exactly, thanks Odinaka. A small extra prompt is a much better failure
than a silent approval. I'll keep tightening the edge cases.
Edge cases are where that rule earns its keep. A truthy string like "yes" slipping through would be exactly the quiet failure it is meant to stop.
Elicitation keeps the model out of the loop, and SECURITY-MODEL.md already says a client that auto-approves prompts removes the gate. In Claude Code you do not need a compromised client for that. The hooks reference has an Elicitation event, matched on the MCP server name, and a hook that exits 0 with hookSpecificOutput action "accept" plus content answers the request with no dialog shown. Any user settings file can register one, and so can a project .claude/settings.json in a cloned repo once the folder is trusted. The server only receives accept, so it cannot tell a click from a script. It may be worth naming this case next to the malicious-client item, and logging the event as something like "client-accepted" instead of "approved", so the audit file does not claim a person. Which other clients let a script answer elicitation?
This is a great catch, and I didn't know about the Elicitation hook. The
server only sees "accept", so it can't tell a person from a script. I'll
add this to SECURITY-MODEL.md next to the malicious-client item. I'll
also change the audit log to record "client-accepted" instead of
"approved", so the file doesn't claim a human was involved. For
irreversible tools, the allowlist and policy checks should run before any
elicitation, so a scripted accept can't override them. I haven't checked
which other clients let a script answer elicitation. If you know of any,
tell me and I'll list them.
Nice approach: splitting preview from execute makes the approval step reviewable instead of a prompt-level hope. One tip that pairs well with it is to log the preview summary together with the final decision, so you can later audit why an action was approved or declined. Have you thought about letting users set per-path allowlists, so routine deletes in a temp folder skip the prompt?
Thanks, logging the preview summary next to the final decision is a good
idea, and I'll add it to the audit entries. Per-path allowlists are
already in. Anything on the allowlist skips the prompt, and the allowlist
check runs before the approval threshold. Routine deletes in a temp
folder are exactly the use case. The project is open source, so if you
want to add a feature or improve this, PRs are welcome. Please contribute
if you're interested.
Cough Cough... Git-Checkout... When it didnt commit any work in that file... For hours...
Ouch, hours of uncommitted work gone is exactly the case I'm worried
about. A checkout over uncommitted changes is destructive, and a "run git
checkout" prompt doesn't show that. A good preview would say "this will
discard uncommitted changes in these files" and list them. I want to add
a check that detects uncommitted work and refuses or warns.
And that's the problem, Claude prompts, but people just accept, because it's happens too regularly, that they just dont bother reading it. Or Claude makes it so ambiguous, that they dont even understand what's going on.
I’d also put changes to the guardrails themselves on the restricted list, like increasing spending limits or modifying an allowlist. If an agent can change its own limits, those limits don’t offer much protection. Would you require human approval for these every time, even for small amounts?
@saurabh_pathak394 Numbers are the part I would test first. Payloads that treat an integer and its float form as equal, or the same value in exponent notation, still need one digest, otherwise the bug you are fixing stays.
One extra thing I'd bind the preview to is the resource version or ETag. The same tool and arguments can still act on a materially different state if the resource changes between approval and execution. If the version changed, expire the approval and require a fresh preview. That makes approval authorize one specific state transition, not just one function call.
the preview step is also the right spot to show cost for paid tools. if the plan carries the max charge the person approves the bill and not only the action, and a run that would go past it never starts