Approval gates: deciding which operations deserve a human
Ask for permission on everything and users hold down 'yes'. Ask for nothing and it's terrifying. Here's how rysh splits the difference.
Every agentic tool eventually faces the same design fork.
Ask the human for permission on everything, and you train them to hold down the "yes" key — congratulations, your safety mechanism is now a metronome. Ask for permission on nothing, and you've handed a probabilistic text generator write access to prod. Both ends of the spectrum fail the same way: the human stops being a meaningful part of the loop.
The actual design problem isn't "should agents ask permission." It's which operations deserve a human — and how to keep approvals rare enough that each one still carries signal.
Here's how rysh answers it, and the reasoning underneath, because the reasoning transfers to any agent stack you might build or buy.
Two kinds of dangerous, two kinds of gate
Not all risky operations are risky in the same way, so rysh uses two different approval strategies.
Preview-first — for operations whose outcome can be shown before it happens. File edits and file writes work this way: the agent computes the change, rysh renders the exact diff in the pane — green additions, red removals, cyan hunk headers — and nothing touches disk until you approve.
[tool] edit internal/auth/session.go
@@ -88,9 +88,12 @@
- token := uuid.New().String()
+ token, err := generateSecureToken(32)
+ if err != nil {
+ return nil, fmt.Errorf("token gen: %w", err)
+ }
[preview only — awaiting approval y/n]
You're not approving an intention ("may I improve session tokens?"). You're approving an artifact. Intentions always sound reasonable; artifacts can be wrong in ways you can actually see.
Pre-approval — for operations whose side effects can't be previewed, only described. git_commit works this way, and so does any shell command that isn't provably safe: rysh shows you exactly what will run, waits, and only then executes. You can't "preview" a command's effect on the world, so the gate moves before execution instead.
The distinction sounds obvious written down. Most tools don't make it — they either gate everything the same way or auto-run everything the same way.
The bash allowlist: safe by default
The most interesting gate is the shell one, because bash is where agents do most of their real work — and where a single command can ruin your week.
rysh ships an allowlist: read-only and idempotent commands run without a prompt. git status, git diff, git log, ls, tree, find, grep, go build, go test, go vet, ps, lsof — these execute freely, because gating them would generate forty prompts an hour and destroy your attention budget.
Everything else prompts. Specifically, a command requires approval if it:
- mutates (
rm,mv, package installs,git push…) - redirects (
>can turnechointo a file write) - uses command substitution (
$(…)can smuggle anything) - chains into unsafe territory (
ls && rm -rf …— the safe prefix doesn't launder the rest) - or simply isn't recognized.
That last one is the important bit: the allowlist fails closed. An exotic command the classifier has never seen doesn't get the benefit of the doubt — it gets a prompt. The failure mode of the heuristic is an unnecessary question, not an unauthorized rm.
The attention math
Why bother with the split at all? Because approval fatigue is not a UX annoyance — it's a security failure with a delay on it.
If every grep asks permission, by Tuesday you're approving on reflex, and on Friday you reflex-approve the one that mattered. The gate didn't fail; you failed, predictably, because the gate spent your attention on noise.
rysh's design goal is that a prompt should be rare enough to read and consequential enough to deserve reading. Reads are free. Writes show diffs. Commands with side effects show themselves before running. When something interrupts you, it's because reversal would be expensive.
One small detail I'm fond of: the diffs are ANSI-coloured for your terminal, but the copy that goes back to the model is plain text, and the copy that flows to shared-output streams is ANSI-stripped — so a teammate watching your pane remotely still sees a readable diff. Approval artifacts are for humans; every rendering path treats them that way.
What gates don't give you
Honesty section, because this is dev.to and you can smell a pitch:
- Approval ≠ correctness. You can read a diff and still approve a bug. Gates give you the opportunity to catch mistakes at the cheap point, not a guarantee you will. (That's also why every tool call is visible in the pane — review doesn't end at the gate.)
- The allowlist is a heuristic. It's built to fail closed, and unrecognized commands always prompt — but "safe" classifications are judgment calls we keep tightening. If you find a hole, I genuinely want the comment.
- A gate can be waved through. No tool fixes a user who stops reading. The most a tool can do is make reading cheap and rare — that's the whole design.
The transferable checklist
Whatever agent stack you use, the operations that deserve a human are boringly consistent:
- Anything that writes to disk → show the artifact first.
- Anything with network side effects → show the command first.
- Anything touching credentials → don't just gate it; redact it.
- Anything irreversible (history rewrites, deletes, sends) → gate it, always.
- Everything read-only → get out of the way.
If your current tool can't tell you which of its operations fall in which bucket, that's your answer about how much thought went into the question.
rysh is in private beta — self-hostable, your infra, your Anthropic key, built on Claude. We're recruiting design partners to pressure-test exactly these gates against real workflows: free access, direct line to the founder. Apply here.
Related: Every tool call visible · Is it safe to let an agent loose on your systems? An honest checklist