'Approve All' Is How Your Agent Ships the Dangerous One
DEV Community

'Approve All' Is How Your Agent Ships the Dangerous One

The One Idea

Auto-approve reads. Gate writes. The line isn't risky vs. safe - it's does this change state.

Disclosure up front: I build agentproto, one of the tools in this market. The primitives in the second half are real and the commands are checkable; the problem in the first half stands on its own. Corrections welcome - file an issue.

You gave your coding agent a shell. Now you live in one of two bad houses.

In the first house, every risky command stops and asks you. Can I run git push? Can I delete this file? Can I curl this? You approve, approve, approve - and somewhere around the two-hundredth reflexive yes, the one request that actually mattered slides through on muscle memory.

In the second house, you got tired of that and ran the agent with --dangerously-skip-permissions. Now it never asks. It also never asks before the rm -rf in the wrong directory, or the DROP TABLE against the wrong DATABASE_URL, while you're at lunch.

Both houses share one bug: they treat "should the agent need approval?" as a property of your attention - how much you're willing to click - instead of a property of the action. Fix that, and the whole problem changes shape.

The Line Everyone Draws in the Wrong Place

Ask ten teams where the approval boundary goes and you'll get three wrong answers.

  • By tool category: "shell is dangerous, file-read is fine" - except cat ~/.aws/credentials is a read that exfiltrates and echo can clobber a file.
  • By reversibility: "block the irreversible stuff" - except you can't reliably tell at call time what's reversible.
  • By vibes: skip-all until something breaks.

The boundary that actually holds is simpler and it's not about the tool at all: Draw it at state mutation.

One widely-cited write-up on human-oversight for code agents (Niklas Heidloff) lands on the exact rule: auto-approve only the commands that don't change state; gate the ones that do. Not by tool, not by reversibility - by whether the world is different afterward. A pure read can run free. A write waits.

And here's why "just approve each one" was never going to save you, stated by the people who studied it: manual-approve, skip-all, and static allowlists all fail the same way. Review every action and you hit approval fatigue - after hundreds of reflexive yeses the dangerous request gets one too. Skip everything and you've armed the agent. A static allowlist rots the moment the agent needs something new. The failure isn't your discipline; it's asking a human to be the runtime permission check. That's the same wall from the trust piece, wearing a different hat: you are the serial bottleneck, and "approve each action" just moves the bottleneck to your clicking finger.

How Do You Gate a Risky Call Today?

Place yourself before you build. Four honest answers, each with its ceiling.

  • You approve each one. Every gated call pings you; you click yes. Safe-ish, and completely unscalable - this is the approval-fatigue trap, and it caps at one agent you babysit full-time. Ceiling: you, again.
  • You skip all of it. --dangerously-skip-permissions, YOLO mode. Fast, fully unattended, and one hallucinated path from a very bad afternoon. Ceiling: the first destructive command nobody caught.
  • You keep a static allowlist. A file of pre-blessed commands. Better - but it's frozen; the agent hits something new and either stalls or you widen the list until it's *. Ceiling: it can't tell a read from a write, so it over- or under-blocks.
  • You classify by risk and relay only the writes. Read-only calls auto-approve; state-changing calls pause and route to a decision - a human or a policy - and the important ones surface instead of drowning. This is the only one that scales, and it's the one you can build.

The rest of this piece wires it. Where are you? If your honest answer is "skip-all, and I hope," you don't have an autonomy setup - you have an unexploded one.

Everything below moves the yes/no off your attention and into the architecture.

Rung 1: Make the Tool Declare Its Own Risk

The relay only works if a call knows whether it mutates before it runs. So put the risk class on the tool contract, not in a human's head.

In agentproto a tool declares what it changes; a read-only tool mutates nothing and approves itself:

// a read-only tool - nothing changes, so it runs free
export const knowledgeSearch = defineTool({
  id: "knowledge.search",
  // …schema…
  mutates: [],          // touches no state
  approval: "auto",     // β‡’ auto-approved, never interrupts you
})

// a write tool - declares the mutation, so it must be cleared
export const applyPatch = defineTool({
  id: "fs.apply_patch",
  // …schema…
  mutates: ["fs"],      // changes files on disk
  approval: "gated",    // β‡’ pauses for a decision before it runs
})

That's "auto-approve reads, gate writes" as two lines of contract instead of a policy you enforce by clicking. knowledge.search from the knowledge piece is literally mutates: [] for exactly this reason. The boundary is now data, so a machine can enforce it - which is the whole point of making approval, in the words of one human-in-the-loop build (Nylas), a first-class architectural feature, not an afterthought you bolt on per tool.

Rung 2: The Permission Relay - A Pause, Not a Wall

Now the gated calls need somewhere to go that isn't a modal blocking your terminal. agentproto turns a pending write into an item in a cross-session inbox: the agent pauses, the decision waits, and anyone - or anything - can answer it.

# what's waiting on a decision right now?
permissions_list
# β†’ [{ id: "perm_9f", sessionId: "sess_42",
#      tool: "fs.apply_patch", summary: "write migrations/003_add_index.sql" }]

# answer it - approve once, approve-always, or deny
permissions_respond { id: "perm_9f", decision: "approve" }

Two things this buys that a terminal prompt doesn't.

First, it's out of band: the approval isn't trapped in one attached session, so a supervisor (or you, from your phone) can clear it without sitting in front of the agent.

Second, it's programmable: decision is just approve / deny, so the responder can be a human for the calls that need judgment and a policy for the calls that don't. The relay is the L2 rung, built. The supervision ladder maps the whole climb from you watch to a gate decides; this is the L2 "permission relay" rung as actual wiring.

Read-only auto-approves (Rung 1), risky writes land here, and only the genuinely judgment-worthy ones ever reach a person. Approval fatigue dies because the reads never showed up in the first place.

Relaying the write to a human is fine for a DROP TABLE. But for "did this diff actually fix the bug," a human clicking approve is just a slower version of the agent grading itself. Some writes need a check, not a click.

Rung 3: Put an External Check on the Write - and Stage the Commit Behind It

The top rung replaces the human judgment call with something that scales: a gate that runs outside the working agent and has to say yes before the write lands. You attach it to the session, it fires at the end of the agent's turn, and its verdict - not the agent's self-report - decides what happens next.

// attach a gate to the session; a green result stages a commit behind YOUR ack
attach_policy({
  sessionId: "sess_42",
  then: "commit",
  gate: {
    // a shell gate: exit 0 = pass
    command: "pnpm",
    args: ["test", "--run", "payments"]
  },
  commit: {
    paths: ["src/payments/retry.ts"],  // stages EXACTLY these - never `git add -A`
    message: "fix: cap payment retries at 3 (incident #2026-04)",
    requireHumanAck: true  // green gate β‡’ waits for your yes
  }
})

The lifecycle is the part that matters: the gate runs, and on green the policy doesn't commit - it moves to awaiting-ack and emits policy:commit-ready. The commit does not happen until you call ack_policy({ policyId, approve: true }), which stages only the listed paths (git add -- <paths>, never -A, never a glob), commits, and emits policy:committed. It never pushes. The agent cannot reach past the gate to the commit; the gate cannot reach past your ack to the push.

When exit codes can't judge, spawn a skeptic. Swap the shell gate for gate: { judge: { adapter, prompt } } and agentproto runs a short-lived LLM reviewer that ends in VERDICT: PASS|FAIL - fail-safe, so a timeout or an unparsable answer counts as FAIL. Prompt it to refute, not admire ("try to prove this diff did NOT fix the bug"). That's the kill-the-loop lesson in one primitive: the check has to sit outside the loop it's checking, or it rubber-stamps.

You now have the full plane: reads run free, writes relay, load-bearing writes wait on an external verdict, and the irreversible act - the commit - is staged behind one human yes.

The Honest Edges (The Gotchas That Bit Me)

This is real infrastructure, which means it has sharp corners. Four I hit live, so you don't:

  • The gate runs against an allowlist, and test isn't on it. Shell gates go through the same default-deny allowlist as any command (.agentproto/allowed-commands.json). test -f x fails with not in allowlist β†’ the policy goes blocked. Use an allowlisted binary - ls x, not test -f x; pnpm / node for tests.
  • The gate's working directory is anchored to the workspace. A session whose cwd is outside the workspace fails the gate with cwd escapes the workspace. If you run agents in per-feature worktrees outside the repo root, shell gates are effectively unusable - fall back to then: "emit" (milestone only) and verify the result yourself with git / gh.
  • Attach before the turn ends. The gate fires on turn-end; attach the policy before the session finishes its turn (spawn idle β†’ attach β†’ prompt), or you race the event and miss it.
  • The durable auto-supervisor is still volatile. The RoutineRunner that would babysit this across many steps is an in-memory MVP today - a daemon restart drops in-flight runs. The pieces that survive are the ones above: attach_policy, the event bus, the ack. Don't build a control tower on the part that isn't load-bearing yet.

And the Fair Comparison

Because credibility depends on it: Claude Code already ships a good version of Rung 1–2 - per-command permission prompts and an allowlist, built in, zero setup. If you live entirely in one vendor's agent, you may not need to wire any of this.

Two things you're trading for that convenience: the approval dies with the session (close the terminal, lose the relay), and it's single-vendor - it won't clear a pending write from your Codex agent or a cheap local model. An approval plane that spans every agent you run, and survives your laptop sleeping, is the part a per-tool prompt can't give you.

The broader principle underneath all of it is OWASP's least agency: grant the minimum autonomy, tools, and credential scope the task needs - and make the grant a decision the architecture enforces, not one you re-make by clicking.

Give It the Keys, Keep the Ignition

Autonomy was never the scary part. An agent that can read your whole codebase, run your tests, and draft the fix is exactly what you want. The scary part is the write - the one command that changes something you can't un-change - landing because you were too tired to catch it or too far away to be asked.

So stop deciding approval with your attention and start deciding it with the action. Auto-approve the reads; they can't hurt you. Relay the writes; a human or a policy clears them. Put an external check on the writes that matter, and stage the one irreversible act - the commit - behind a single deliberate yes.

The agent proposes. Something outside the agent disposes. That's the whole plane.

Give your agent the keys. Just keep the ignition where it can't reach it.

If your setup draws the approval line somewhere smarter than state mutation, or you've made approve-each actually scale, tell me where - I'll fix the piece.


The series - Orchestration, Honestly

Ten pieces, one argument. Start anywhere; each one cross-links the rest.

Piece The one idea
1 You can't parallelize the trust - Amdahl's Law: why your fifth agent slows you down
2 Harness engineering - you rent the model; the harness is the part you own
3 The supervision ladder - five rungs of trusting an agent you don't watch
4 The approval plane (you're here) - auto-approve reads, gate writes - wire the line between
5 Kill the loop - why "keep going until done" compounds a wrong turn
6 Route by cost - plan expensive, execute cheap, verify independently
7 Files with contracts - the interop layer every agent system reinvents
8 Knowledge is power - give your agent your knowledge, not the internet's average
9 Paseo, hands-on - a full real-session review of the daemon
10 9 orchestrators, compared - the tool-by-tool teardown + a decision table

Written by the maintainer of agentproto (Apache-2.0, source). Same contract as our /compare page - dated facts, named strengths, corrections by issue. Got something wrong? File an issue.

Comments

No comments yet. Start the discussion.