03:12. A message in the incident channel: "Agent wants to roll back payments-api to v2.13. Approve?"
A thumbs up emoji. Two seconds. The rollback was wrong, and it took another forty minutes to find out, because the approver was half awake and had no idea what the agent had actually done.
Everyone building incident agents this month is adding exactly this feature: a human approval step before the agent touches production. Sixty-eight posts on DEV in the last week alone, most of them integrations with some chat product. It is the control OWASP recommends for high-risk agent actions, and it is the right instinct. The implementation is usually theater.
A gate is a decision, not a click
An approval is a security control only if the person clicking can answer four questions in under thirty seconds:
What exactly will change? Not "roll back the service." The plan: image tag, replica count, migration state, and whether anything is being reverted that cannot be re-applied.
What is the blast radius? Which services, which customer tiers, how many rows, whether the previous attempt at this is in the incident timeline.
Can we undo it? Reversibility, and the time it takes to reverse. A rollback across a schema migration is not the same action as a rollback of application code, and the prompt should not look identical for both.
Is this the plan that was reviewed, or a slightly different one? Hash it.
If the message in Slack contains none of those, you did not build a human gate. You built a notification with a button, and you gave the agent a path to production that costs one emoji.
The change envelope
I make the agent emit a fixed structure before it is allowed to ask for anything. If the structure is incomplete, the approval request does not render.
{ "id": "chg_01J8Z6", "requested_by": "agent/incident-crew", "service": "payments-api", "env": "prod", "plan": { "kind": "rollback", "from_image": "payments-api@sha256:9f2c...", "to_image": "payments-api@sha256:41ab...", "commands": ["kubectl -n prod set image deploy/payments-api api=...@sha256:41ab..."], "migrations_in_scope": [] }, "blast_radius": { "services": ["payments-api"], "customer_tiers": ["all"], "est_requests_affected": 4100 }, "reversible": true, "reverse_plan_eta_seconds": 180, "evidence": ["INC-2291", "deploy/4412"], "plan_sha256": "b7d1e0...", "expires_at": "2026-11-16T03:22:00Z" } Four fields do most of the work: plan_sha256 (the thing you approve is the thing that runs), reversible plus the reversal ETA, blast_radius, and expires_at. That last one matters more than it looks. An approval from 03:12 must not still be valid at 09:40 when the on-call has gone to bed and a different incident is running.
The executor verifies the hash before it applies anything. If the plan changed after the approval, the approval is void. Without that check, your "human approved" audit trail is a claim about an intention, not about an action.
Measure the gate or it isn't one
Two numbers, pulled from the approval log, will tell you whether you have a control or a formality.
Median time from request to approval. If it is under ten seconds, the reviewer is pattern-matching the name of the service, not reading the plan. That is fine for a canary bump and not fine for anything else, and the difference should be visible in the data rather than in your architecture diagram.
Reversal rate within twenty-four hours. How often an approved agent change gets undone by a human shortly after. A high number means the gate is approving things it should be rejecting, and the agent's plans are worse than its confidence suggests.
I also log the approver's identity and the incident they were paged for. When a postmortem says "the agent rolled back the wrong service and a human approved it," the useful question is not why the agent was wrong. It is why a tired person with three other tabs open was asked to be the last line of defense for an irreversible action at 3 a.m.
Default reversible, gate the rest
The better design inverts the usual instinct. Instead of gating everything and hoping the reviewer is sharp, make almost everything reversible and stop gating it:
Ship behind a flag with a kill switch. Canary at 5% with an automatic abort on error-rate burn. Cap the blast radius in the mechanism, not in the prompt: quota limits, a ten-minute change window, a replica ceiling. Then the agent can move fast without asking, because being wrong is cheap and self-limiting.
Keep the human gate for the short list that cannot be undone: schema migrations with data loss, deleting a bucket, rotating a credential that other services cache, force-pushing a branch, anything that touches billing records. That list is usually five items long. Write it down. Make the agent prove which one it is in.
That is a much smaller promise than "human in the loop," and it is the only version I have seen hold up under a real incident.
The part people don't want to hear
"Human in the loop" reads like a safety property. In practice it is a staffing assumption. Every approval queue needs someone awake, competent, and holding enough context to say no. If your incident rotation is three engineers and your agents file twenty change requests a night, you have not added a control. You have added a queue with a latency SLO nobody wrote down.
The gate should be a circuit breaker with a person on the other side of it, not a poll. Build the breaker first. Then decide which changes are expensive enough to wake someone up for, and be honest in the postmortem when the answer was "none of them, we just liked the feeling of approving."
One more thing worth saying plainly: if your approval message is a chat bubble with a service name in it, an attacker with write access to that channel, or to the agent's own context, can produce a plausible request. The envelope does not stop that. It just makes the request falsifiable, which is the most a prompt was ever going to do.
