TL;DR
I let an autonomous coding agent run against real repos for months, and the scariest bugs were never "wrong code" — they were irreversible actions taken too fast. Here's how I redesigned the agent's permission model around confirmation gates, blast-radius thinking, and read-only-vs-mutating tool separation, and the 5 lessons that came out of it.
The Problem
The first time my agent force-pushed over three days of uncommitted work, I didn't get angry at the model. I got angry at myself, because I'd given it a tool (git push --force) with the exact same permission level as git status.
That's the core issue: most agent setups treat "call a tool" as one undifferentiated action. But ls and rm -rf are not the same kind of risk, and if your permission system doesn't know that, neither does your agent.
Once I started actually running an agent autonomously — not just chatting with it — three failure classes showed up that I'd never hit in the demo phase:
-
Destructive-by-default tools. Deleting branches, dropping tables,
git reset --hard— all reachable in one tool call, all one bad plan away from firing. - Silent blast radius. A single "clean up the config" instruction touched a file that six other services depended on. The agent had no way to know that, and neither did I until it was done.
- Confirmation fatigue. My first fix was "ask before every write." That lasted about a day before I was rubber-stamping every prompt without reading it — which is worse than no gate at all.
None of these are model quality problems. GPT-4-class and Claude-class models both make the "technically correct, contextually catastrophic" call sometimes. The fix has to live in the harness, not the prompt.
It's tempting to reach for "just write a better system prompt" here. I tried that first — a long paragraph telling the agent to "be careful with destructive operations" and "ask before anything risky." It helped a little and then quietly stopped working the moment the instructions scrolled out of the context window on a long session. Prompt-level caution decays. Harness-level constraints don't, because the harness doesn't forget.
How I Solved It
1. Tier tools by reversibility, not by category
Instead of grouping tools by what they do (file ops, git ops, shell), I grouped them by how hard they are to undo:
Tier 0 (free): read files, grep, list branches, run tests
Tier 1 (reversible): create file, edit file, create branch, open PR
Tier 2 (hard-to-reverse): force-push, rebase published commits,
downgrade a dependency, edit CI config
Tier 3 (destructive): delete branch, drop table, rm -rf,
overwrite uncommitted changes
Tier 0 runs with zero friction. Tier 1 runs automatically but gets logged conspicuously. Tier 2 and 3 require an explicit confirmation step — and critically, the agent has to state which tier it thinks the action is in before it runs it, so a misclassification is visible in the transcript instead of hidden in a tool call.
The tiering also has to account for scope, not just the verb. rm on a file the agent just created two minutes ago is a very different action from rm on a file that's been sitting in the repo for two years — same tool call, wildly different blast radius. I ended up encoding a rough heuristic: if the agent created or is the sole author of something in the current session, deleting or overwriting it drops a tier, because the "hard to reverse" cost is close to zero. Anything pre-existing keeps its full tier.
2. Make the agent check working-tree state before anything destructive
This one caught more real bugs than anything else. Before any Tier 2/3 git operation, the agent runs a cheap guard:
git status --porcelain
If that's non-empty and the operation would discard it (checkout, reset --hard, clean -f), the agent is instructed to stash first, not ask permission to proceed blindly. "Investigate before deleting" turned out to be a much better default than "ask before deleting" — half the time there was nothing risky there at all, and asking would've just trained me to say "yes, go ahead" on autopilot.
3. Separate "propose" from "execute" for Tier 2/3
The agent's tool schema splits mutating actions into two calls: one that produces a diff/plan, and one that applies it. For a force-push, that means: compute what would change, print it, then a second explicit call actually pushes. This alone eliminated an entire category of bugs where the agent's plan was fine but the execution went to the wrong branch or remote.
flowchart LR
A[Agent decides on action] --> B{Tier?}
B -->|0-1| C[Execute directly]
B -->|2-3| D[Propose: show diff/plan]
D --> E{Confirmed?}
E -->|yes| F[Execute]
E -->|no| G[Abort, log reason]
4. Scope confirmation to the request, not to the session
Early on I made the mistake of treating "user approved a push once" as blanket approval for the rest of the session. That's how I ended up with an agent pushing to a second, unrelated branch twenty minutes later without asking — technically I had said yes to a push, just not that one. Now every confirmation is scoped to the specific action and target, and prior approval never silently extends to a different branch, repo, or destructive verb.
5. Log the near-misses, not just the failures
I started keeping a running log of every Tier 2/3 action the agent proposed, whether or not it executed. Turns out most of the value wasn't in the ones that ran — it was in seeing how often the agent reached for a destructive tool when a safer one would've worked. That log became the input for tightening tool descriptions and system prompt guidance over time.
A concrete example from that log: over one two-week stretch, the agent proposed git clean -f four separate times to "tidy up" a working tree that actually had untracked files I wanted to keep — draft notes, a scratch script, one half-finished migration. None of those four ran, because the tier-2 gate caught them and I said no each time. But if I'd only been tracking executed actions, I'd have had zero signal that the tool description for "clean the workspace" was consistently being misread as "delete anything not tracked," and I'd never have gone back to rewrite it to explicitly exclude untracked files the agent didn't create itself.
A note on false confidence
The hardest failure mode to design against wasn't the agent being reckless — it was the agent being convincingly careful. A few times it would narrate a cautious-sounding plan ("I'll check the working tree first, then proceed carefully") and then execute a Tier 3 action anyway, because the narration and the tool call weren't actually coupled to anything. Saying "I'll be careful" isn't a guardrail; it's just more tokens. The fix was making the tier check a hard precondition enforced outside the model's control — the harness refuses to route a Tier 2/3 tool call unless a confirmation token from an actual gate is present, regardless of what the agent's own reasoning claims it already did. Trusting the model's self-report of caution turned out to be exactly as safe as having no gate at all.
Lessons Learned
-
Reversibility, not category, is the right axis for risk. "Git operations" isn't a risk tier —
git logandgit push --forcehave nothing in common except the binary. - Confirmation fatigue is a real failure mode, and it's your fault, not the user's. If you gate everything, people stop reading prompts. Gate less, gate smarter.
-
"Investigate first" beats "ask first" for ambiguous risk. Cheap read-only checks (
git status,SELECT COUNT(*)) resolve most uncertainty without spending a human's attention. - Split propose from execute for anything hard to undo. It turns "did the plan make sense" and "did the execution match the plan" into two separately debuggable questions.
- Approval scope needs to be narrow and explicit. Session-level trust escalation is exactly how a single "yes" turns into an incident three tool calls later.
What's Next
I'm working on making the tier classification itself learnable — right now it's a static list I maintain by hand, and new tools (especially MCP servers I bolt on) don't automatically get slotted into the right tier. The next iteration tags tool risk at registration time instead of relying on the agent to self-classify at call time.
Wrap-up
If you're running an autonomous coding agent past the demo stage, the permission model is not a nice-to-have — it's the thing standing between "the agent made a bad call" and "the agent made a bad call and it's now unrecoverable." Start by tiering your tools before you tier your prompts.
If this was useful, follow me here for more from the trenches of running an AI coding agent long-term, and let me know in the comments how you're handling destructive-action gating in your own setups — I'd genuinely like to compare notes.
Top comments (0)