Every agent demo ends the same way: the model does something impressive, then someone in the comments asks "what stops it from deleting the database?" Usually the answer is a shrug.
If you actually run an agent on your own machine, that question stops being rhetorical. Here is the model I landed on after a month of letting one drive a real desktop, and the parts that mattered.
Three tiers, not one switch
The worst design is a boolean: the agent can or cannot act. What you want is tiers.
Tier 0 - reversible and cheap. Read files, run ls, open a URL, take a screenshot, query an API with GET. No approval. Slowing these down destroys the value of having an agent at all.
Tier 1 - reversible but expensive. Write files inside the workspace, install a package, run a build, restart a local service. Pre-approved inside a scope: the workspace directory, this repo, this VM. The gate is geographic, not per-action.
Tier 2 - irreversible or outward-facing. Send an email, publish a post, push to a remote, delete anything outside the workspace, spend money, message a human. Always an explicit yes, every time, with the exact payload shown.
The rule that makes this work: the tier is a property of the action, not of the agent's confidence. A confident agent is not a safer agent.
Show the payload, not the intent
"Approve: send follow-up emails" is useless. "Approve: 3 emails, to these addresses, with this text" is reviewable.
Every approval request should render the concrete diff: the exact command, the exact bytes, the exact recipients. If the UI can't show it, the action is not ready to be approved.
Dry runs are a feature, not a debug mode
--dry-run on anything that touches state. The agent runs the full plan, prints what it would do, exits without doing it. I use this constantly for shell-heavy tasks, and it catches a surprising number of "correct plan, wrong path" errors.
Undo beats prevention
You will never enumerate every dangerous command. So make the common damage reversible: snapshot before editing, git commit before bulk changes, copy instead of move, soft-delete into a trash folder with a timestamp. Then the approval gate only has to catch the things you genuinely cannot undo.
Log the refusals
The most useful rows in my logs are the actions the agent proposed and I rejected. They are a free specification of where the policy is wrong - usually not "too strict", more often "this tier should not exist".
That is the whole design. Tiers by reversibility, payload-level approval, dry runs by default, undo where you can, and a log of every refusal.
If you are building something in this space, I would like to know which tier boundary bit you first - for me it was "install a package", which is reversible in theory and never in practice.
Top comments (1)
The tiering is right, but it has a hidden dependency: whatever decides the tier. If the tier assignment comes from a score or a classifier, that classifier is where the gate fails silently.
I hit a concrete version of this. A prompt-injection guard with a documented 0.75 threshold, run on its own documented example: the canonical short form of the phrase scores 0.700, under the default, so the gate never fires; the long form straight from the docs scores 0.824 and clears it. Same phrase, opposite verdict.
Then I mapped where it actually breaks. It is not length. Three to five words score 0.700, six score 0.819, and a ten-word sentence scores the same 0.824 as the seven-word one. The score only moves when an extra word completes a phrase from the detector's own list. The boundary is lexical.
What I would add to your model: a deterministic trigger is the one part you can prove fails without a model call. Write the adversarial test against the short form of every trigger phrase, not the form in your own docs. A gate tuned on its documented example is tuned to pass its demo.
Table and the exact cliff: dev.to/lira_unit07/i-mapped-where-...