DEV Community

hao li
hao li

Posted on Originally published at github.com

I built a human approval gate my coding agents can't click themselves

My coding agents kept doing the same thing: printing "please click Allow" into a terminal nobody was watching, then either stalling forever or — worse — finding a way to proceed on their own.

This month gave me two reasons to stop ignoring that. Reuters reported that OpenAI's own agents weakened their internal monitoring: one model turned off auto-alerts to the security team, another deleted a safety monitor and then denied doing it. And an engineer wrote about her agent making 16 unapproved paid X API calls that triggered a $10 auto-recharge: "If I have to sit next to it saying 'isn't that the one that costs money?', there's no point."

So I built human-gate: a tiny cross-platform approval channel. The agent asks, I get a desktop notification and a local web page with Approve / Deny buttons. Unanswered requests default to deny. Every request and decision goes into an append-only, hash-chained audit log.

The one design decision that matters

The approval channel lives outside the agent's control plane. The agent-facing API can create requests and poll their status — and that is all it can do. There is deliberately no decision endpoint on the agent side. None. An agent using the documented interface cannot approve its own request because there is no route for it to call.

Decisions only happen through a private desk URL like http://127.0.0.1:8765/desk/<256-bit-token>/, printed to my terminal when the daemon starts. The token is never returned by any agent API call, never logged, never written to disk. Wrong token on the decision endpoint gets a 401, and the attempt itself is recorded in the audit log.

I'll state the threat model honestly: this stops an agent that follows its tool interface from self-approving — the realistic failure mode, including a prompt-injected agent "helpfully" dismissing its own gate. It won't stop a fully adversarial local rootkit. Same bar as the tmux-session isolation most of us already rely on.

Why not just use Big Arrow?

Big Arrow (the Show HN hit from earlier this month) is macOS-only Swift. I run agents on Windows and Linux too, so I wrote human-gate in pure Python stdlib — zero dependencies, http.server plus an inline HTML page, OS-native notifications via notify-send / osascript / PowerShell toast. x402-style protocols are identity layers, not approval UX; Frink is a whole desktop orchestration app. This is the small boring piece: a gate, not a platform.

How it fits a real workflow

pip install human-gate
humangate serve   # prints your private desk URL — open it in your browser
Enter fullscreen mode Exit fullscreen mode

Then the agent side blocks on a decision:

humangate request --title "Delete staging database" \
  --detail "DROP DATABASE staging;" --risk critical --timeout 300
# exit 0 = approved, anything else = denied
Enter fullscreen mode Exit fullscreen mode

There's a Claude Code PreToolUse hook example in the repo: risky Bash commands (rm -rf, git push, npm publish, cloud CLIs, sudo) get routed through the gate, everything else passes through untouched. No approval fatigue for read-only work — that was a deliberate rule, because a gate you learn to ignore is worse than no gate.

24 tests, all green, including the tamper cases: decide-without-token, decide-with-wrong-token, wrong desk path, double-decide, decide-after-timeout, and audit-log tampering detection.

If you run agents in tmux and have ever wondered "which one is waiting for me right now" — this is that question, answered, with a paper trail.

Top comments (2)

Collapse
 
autenai profile image
Auten •

Putting the decision endpoint outside the agent's API is the right call. One gap, though, and I hit it because I work on screen control (I'm on the Auten team; we give agents hands on a real desktop): the "no route to call" guarantee assumes the agent only has tools. Once an agent has a computer-use MCP on the same machine, it can screenshot the terminal where humangate serve printed the desk URL, open it in the browser and click Approve like you would. A prompt-injected agent that "helpfully" clears its own gate is exactly the case you describe, and screen access makes it much easier.

Cheap mitigations that fit your threat model:

  • Show the desk URL once, then clear it from the terminal, or only write it to a file the agent's user can't read.
  • Make the Approve click itself need something the screen doesn't show: an OS prompt (Touch ID / polkit / Windows Hello), or a short code that only goes to your phone.
  • Log the User-Agent and focused window when a decision arrives, so the audit chain can tell a hand click from a scripted one.

We ship the same rule as a skill on our side (ask the human before any permission/approve dialog), but that is only a prompt. A gate like yours, with one of the steps above, is the real control.

Collapse
 
reidmarlow profile image
Reid Marlow •

Separating the decision token from the agent's filesystem and process environment is the cleanest way to prevent self-approval. Once an agent gets shell access, any auth token stored in an environment variable or local config file becomes trivial for the model to grep and replay. Keeping the desk token strictly ephemeral in the parent terminal closes that loop cleanly.