How AI agent approval becomes a real control
Place AI agent approval at the last responsible moment, log exact intent and results, design reversals, and keep some operations human-only.

An agent with write access should earn approval for a specific side effect, not receive a blessing for a general plan. The useful checkpoint sits after the agent has resolved its intent into exact targets and parameters, but before the first external system accepts a change. Anything earlier asks a person to approve a guess. Anything later turns approval into incident review.
That sounds simple until one task expands into forty API calls, a production state changes while the approval dialog is open, or the alleged rollback cannot recreate what was overwritten. A meaningful control needs four properties: the reviewer sees the proposed effect, the approved action cannot silently mutate, the system records what happened, and the operator has a tested recovery path. Some effects fail that test and should remain manual.
I treat write access as a collection of narrow capabilities rather than one permission. Creating a draft record and publishing it are different capabilities. Preparing a database migration and applying it are different capabilities. Once those boundaries exist, the approval step can protect the operation that carries the consequence instead of interrupting every harmless calculation.
Put approval beside the side effect
The approval gate belongs immediately before the component that can commit the write. Let the agent read, reason, calculate a patch, run validation, and produce a dry-run result without interruption. Stop it when it asks the trusted execution layer to turn that proposal into an external effect. The executor, not the model, must enforce the stop.
Approval at prompt time is too early. A request such as "clean up duplicate customer records" does not say which records will merge, which values will win, or how many downstream references will move. Approval after tool selection is often still too early because the tool arguments may contain a broad query. The person needs the resolved set of targets and the effect on each one.
The gate also has to be inside the authorization boundary. If the agent can call the production API directly and a separate user interface merely asks for consent, the dialog is theater. Give the agent credentials that can prepare an action, then require the executor to exchange an approval token for the stronger, short-lived capability used to commit it. Bind that token to a digest of the canonical action payload. A changed target, parameter, or precondition produces a new digest and needs new approval.
For a batch, approve the bounded batch rather than a vague objective. Show the count, enumerate targets when the list is reasonably small, and provide a machine-readable attachment when it is large. Set a maximum count and maximum cost in policy. If discovery finds more work than the reviewer approved, the executor must stop instead of treating the old answer as permission to continue.
The practical test is blunt: after clicking approve, can the reviewer say exactly which system will change, which objects will change, and what invariant should still hold? If any part remains unknown, the proposal is not ready for approval.
Show the reviewer the resolved action
A reviewer should see an effect-oriented rendering made from the same canonical payload the executor will consume. Do not ask them to interpret chain-of-thought, a chat transcript, or a model-written promise. Those materials can provide context, but they are not the contract. The contract is the normalized action.
For a file change, show the repository, revision, file paths, diff, generated files, and checks that ran. For SQL, show the database identity, statement or migration digest, transaction mode, estimated affected rows from a safe plan, and any lock implications you can determine. For a message, show recipients, visible content, attachments, and whether sending can trigger another system. For cloud infrastructure, render the resource plan and identify replacements or deletions separately.
The approval screen should answer four questions without requiring expansion:
- What exact state will change?
- Why did the agent choose these targets?
- Which checks passed, and which checks did not run?
- What recovery operation is available if the result is wrong?
Keep the agent's explanation separate from facts measured by tools. "The test suite passed" should come from the test runner with an exit status and artifact digest. "This change is low risk" is an assessment, and the interface should label it that way. I have seen reviewers trust fluent explanations while missing a destructive flag in the actual arguments. Put the arguments first.
Approval must also have a lifetime. Include the base revision, record version, ETag, schema version, or another precondition that proves the reviewed world still exists. A ten-minute-old file patch may be safe to apply with a clean revision check. A ten-minute-old decision to cancel a payment or rotate a production secret may already be stale. Expiry should follow the operation, not a universal timer.
Do not let the agent approve its own rendering. Build the view in trusted code from a typed action schema, escape untrusted text, and make omitted fields visible. A blank recipient field should appear as blank, not disappear from the card. Hidden defaults are still parameters and belong in the payload digest.
Classify actions before you automate them
A useful policy classifies operations by consequence and reversibility before any agent requests them. The model may suggest a class, but trusted code maps a registered action type to its policy. Otherwise a persuasive description can downgrade a dangerous operation.
I use four practical classes. Read-only work needs no approval unless the read itself exposes tightly restricted data. Draft writes can run automatically inside an isolated workspace. Reversible external writes need approval at commit. Irreversible or authority-changing operations stay manual, often with a second person required by the surrounding system.
A compact policy can look like this:
actions:
repo.patch:
mode: approve_at_commit
require: [base_revision, diff_digest, test_run_id]
expires_in: 30m
rollback: revert_commit
customer.merge:
mode: approve_at_commit
require: [source_ids, winner_id, snapshot_id]
max_targets: 20
expires_in: 5m
rollback: restore_snapshot
signing_key.destroy:
mode: human_only
audit_log.delete:
mode: forbidden
The distinction between human_only and forbidden matters. A human may destroy a retired signing key through the key-management console after the organization's normal ceremony. Neither the agent nor its executor should hold that capability. Deleting the audit record that would explain the agent's own behavior has no legitimate place in this automation path, even if a person clicks a button.
Keep several other operations outside autonomous execution: disabling the controls that supervise the agent, expanding the agent's own permissions, changing approval policy, erasing backups, removing the last recovery copy, and sending an irrevocable legal or financial commitment. The exact list depends on the business, but the pattern is stable. An agent must not alter the evidence, authority, or recovery mechanisms that constrain it.
Teams sometimes argue that a second approval makes any action safe. It does not. Two people can approve an unreadable request, and both can miss the same hidden default. Multiple approvers help with separation of duties; they do not repair a bad action contract.
Log an action as a verifiable envelope
Log one durable envelope for every proposed action and append state transitions as it moves through approval and execution. A chat transcript is useful supporting evidence, but it is a poor audit record: it mixes deliberation with instructions, may omit tool defaults, and rarely proves which bytes reached the target system.
NIST SP 800-53 control AU-3 says audit records should establish what happened, when, where, the source, the outcome, and the identity associated with the event. That is a sound minimum, but an agent executor needs more because the proposal and the committed effect can diverge. Record both intent and observed result, tied by stable identifiers.
This is the shape I expect from an execution event:
{
"action_id": "act_01J...",
"run_id": "run_01J...",
"action_type": "repo.patch",
"actor": {"agent_id": "migration-agent", "model_release": "approved-release"},
"requester": {"user_id": "u_1842", "session_id": "s_9031"},
"target": {"repository": "billing", "base_revision": "4b2c..."},
"intent_digest": "sha256:9f3a...",
"policy": {"version": "2026-08-14.3", "decision": "approval_required"},
"approval": {"approver_id": "u_771", "payload_digest": "sha256:9f3a...", "at": "2026-08-14T09:31:22Z"},
"execution": {"started_at": "2026-08-14T09:31:24Z", "executor_id": "exec-prod-2", "attempt": 1},
"result": {"status": "committed", "revision": "51ad...", "changed_files": 7},
"recovery": {"kind": "revert_commit", "handle": "51ad..."}
}
The real event should also include the canonical parameters or a tamper-evident reference to them, tool and connector versions, policy inputs, precondition results, validation artifacts, error codes, and the target system's receipt or request identifier. Record attempts separately. If a network timeout leaves the outcome unknown, write unknown, reconcile against the target, and do not blindly retry a non-idempotent operation.
Protect the log from the actor it records. NIST AU-9 covers protection of audit information and audit tools; the operational consequence is that the agent's write credential must not edit or delete its audit trail. Send events to an append-oriented store with restricted administration, retention rules, clock synchronization, and integrity checks. Redact secrets before storage, but do not turn redaction into omission: record that a sensitive field existed and store a keyed digest or protected reference when investigators may need correlation.
Reversibility must be designed per operation
A change is reversible only if you can name the inverse operation, preserve the data it needs, and demonstrate that the inverse still works after the forward action. A generic "undo available" flag proves nothing. Different systems need different recovery designs.
Source control gives you a revert commit, but a reverted deployment may not reverse a database write already performed by the released code. A database transaction gives clean rollback only until commit. After commit, restoration may require a compensating transaction or point-in-time recovery, both of which can overwrite legitimate work that arrived later. A sent email cannot be unsent in the systems that already received it. A correction is compensation, not reversal.
Before approval, the action contract should name one of these recovery modes:
- Transaction rollback, where the system can abort before exposing the change.
- Version restore, where the old object and its version remain available.
- Compensating action, where a new event semantically offsets the first.
- Forward repair, where operators deploy a corrected change because rollback would damage newer state.
- No reversal, which raises the approval class or keeps the action manual.
Capture before-state selectively. A snapshot of the affected records may make a customer merge recoverable, but copying an entire restricted database into an agent workspace creates a worse problem. Keep snapshots in the target's protected recovery system, encrypt them under separate access, attach retention, and place only the recovery handle in the action record.
Test recovery with the same seriousness as the forward path. For each registered write action, run a fixture through prepare, approve, commit, recover, and compare. Check side effects in queues, caches, search indexes, webhooks, and downstream ledgers. If the test verifies only the primary table, it proves only that the primary table can be restored.
Idempotency is related but different. An idempotency key prevents a retry from applying the same logical operation twice. It does not reverse a bad operation. Use both: stable action IDs for safe retry and explicit recovery handles for correction.
Stale approval is a failed precondition
An approval authorizes one payload against one known state. If either changes, the executor should reject it and ask for a fresh proposal. This is optimistic concurrency applied to human judgment, and it closes a gap that many approval workflows leave open.
Bind file actions to a commit hash, API updates to an ETag or record version, database work to a schema version and constrained predicate, and infrastructure plans to the plan digest plus provider state serial where available. Evaluate preconditions inside the executor immediately before commit. Do not let the agent report that it checked them earlier.
Partial batch failure needs an explicit rule. Atomic batches should roll back all items when one fails. Non-atomic batches should record an outcome for every target and stop when the approved error threshold is crossed. The interface must tell the reviewer which behavior applies. Quietly continuing through a list after several failures turns a bounded approval into an experiment on production.
Long-running work should separate approval of the plan from approval of each dangerous phase. A person can approve generating one thousand candidate edits in an isolated branch. Promotion still needs a fresh gate based on the final diff and current base revision. Reusing the planning approval for deployment collapses two different decisions into one.
Approval tokens should be single use. If execution fails before commit, the system can issue a new request that references the prior action and shows what changed. Replaying the same token after an ambiguous response risks duplicating an effect or applying an old decision to new state.
Give the executor less authority than production
The trusted executor should hold only the capabilities registered in the action policy, not a general production administrator credential. Moving the write key out of the agent process solves little if the executor can turn any model-produced string into an arbitrary API call, shell command, or SQL statement. The boundary needs typed operations with strict parameter validation.
Build one adapter per action type. A customer-status adapter might accept a customer ID, expected record version, and a value from a small enum. It should not accept a raw URL, arbitrary headers, or a free-form query. A repository adapter can apply a validated patch to one named repository without offering a shell. This is more work than exposing a generic connector, and it is the work that makes the approval meaningful.
Constrain the executor at the target as well as in application code. Give its database identity permission to call approved stored procedures rather than write every table. Scope cloud roles to named resource classes and allowed operations. Restrict repository credentials by organization and repository. Use network policy so an action adapter cannot reach unrelated services even when a malformed parameter tries to redirect it.
Treat tool output as untrusted input. An issue description, source comment, database value, or web response can contain text that tries to influence the agent. That is usually discussed as prompt injection, but the control consequence is simpler: content read from a target never becomes authority to write back to it. Only policy and a valid approval token grant authority. The executor parses typed fields and rejects instructions smuggled inside data fields.
Secrets need equally narrow handling. The model rarely needs to see a credential. Let the executor resolve a credential reference after approval, use it for one registered operation, and keep the value out of prompts, previews, errors, and logs. When a target requires a powerful static secret, put a broker in front of it that can mint a short-lived credential with an action-specific scope. If the target cannot support that scope, classify the adapter according to the full power of the credential, not the modest intent of the current request.
An agent must also be unable to register a new action type at runtime. Adapter code, schemas, renderers, recovery handlers, and policy mappings belong in the reviewed control plane. Updating them is a software release with its own human process. Otherwise the agent can bypass a forbidden operation by inventing a friendlier name and routing the same destructive call through it.
Finally, separate execution identity from the human approver. The target system should record that the executor performed the write on behalf of a named requester under a named approval, rather than impersonating the reviewer. That preserves accountability without teaching people that approving a proposal means lending the agent their full session.
Measure whether people can make the decision
Approval quality can be observed. If nearly every request is approved in a few seconds, the workflow may contain low-risk noise or reviewers may be clicking through it. If reviewers repeatedly open raw logs to understand a proposal, the primary rendering is missing information. If expired proposals are routinely reapproved without inspection, the system has turned freshness into another nuisance button.
Collect process measures without grading individual employees for speed. Useful signals include time spent before a decision by action class, rejection reasons, requests returned for missing context, changed payloads after rejection, stale-precondition failures, emergency-path use, and recovery invocations. Compare these signals by action type and interface version. A single global approval rate hides the adapter that causes the trouble.
Ask reviewers for structured rejection reasons, with optional free text. Keep the list short: wrong target, unexpected scope, insufficient evidence, unsafe timing, no credible recovery, and policy mismatch cover most technical decisions. Feed those reasons back into the schema and renderer. If reviewers often choose wrong target, put identity and environment higher on the card. If they choose no credible recovery, stop claiming that action class is reversible until the handler passes a drill.
Queues need ownership and escalation. A request sent to a broad channel invites diffusion of responsibility; everyone assumes someone closer to the system will inspect it. Route by system and action class to a small on-duty group, show who claimed the review, and release the claim if they leave it idle. The requester should see that a proposal is waiting, but should not be able to pressure the interface into choosing a default approval.
Design the dialog for refusal. The reject control should be as available as approve, and closing the dialog must not count as consent. Do not preselect approval, use countdown pressure, or hide destructive details behind collapsed panels. For complex diffs, let the reviewer search and filter while keeping the canonical summary and digest visible. Accessibility is part of the security control because a reviewer who cannot operate the comparison view cannot inspect the action.
Sample approved actions for independent review. The sample should compare the rendered proposal, canonical payload, target receipt, and observed result. It can reveal fields that render ambiguously, adapters that add target-side defaults, and reviewers who were assigned outside their competence. Use the findings to change policy or interfaces, not merely to remind people to be careful.
Approval fatigue is usually a classification failure. Move predictable, low-consequence actions into a narrow automatic policy with limits and monitoring. Combine related changes into a bounded batch when one decision genuinely covers them. Keep individual gates for actions where human context can change the outcome. Fewer serious decisions receive more attention than a stream of ceremonial prompts.
Legacy rewrites need parity evidence at the gate
For a legacy rewrite, approval should attach to promotion of a verified behavioral change, not to every generated file. Agents need room to inspect the whole source tree, map dependencies, generate target code, and run tests in isolation. The consequential moment is when the new service, client, schema, or numeric kernel can reach production traffic or data.
A source diff alone is weak evidence in this setting. Reviewers need the source revision, generated target revision, architecture decisions that affect operations, schema changes, and parity results against recorded production behavior. They also need a clear list of intentional differences. A green build says the new code compiles. It does not say a rewritten month-end calculation agrees with the program that has carried the business for years.
CodeHero reads the full mixed-language codebase and checks rewritten behavior with a parity harness against the customer's recorded production traffic; that evidence belongs beside the promotion request, with any mismatch made impossible to hide behind a summary. For regulated environments, its models can run air-gapped inside the customer perimeter, but the customer's approval and audit controls still decide who may promote the result.
Treat data migration as its own write action. Approve the exact schema version, transformation digest, row scope, validation queries, cutover preconditions, and recovery point. A code promotion approval should not silently authorize a backfill, and a backfill approval should not authorize deleting the old store. Those operations have different failure modes and different recovery clocks.
The same separation applies to architecture modernization. An agent may propose splitting a monolith or replacing a shared file with Postgres, but approval should cover observable consequences: routing changes, data ownership, concurrency behavior, and operational rollback. Otherwise a reviewer is asked to approve a design label instead of a system effect.
Test the control by trying to bypass it
An approval system is ready only after tests prove the agent cannot route around it. A successful happy-path dialog shows that the user interface works. It does not show that every production write passes through the executor or that the token binds to the displayed payload.
Run adversarial cases against the control:
- Change one parameter after approval and confirm the digest check rejects execution.
- Change the target version while the dialog is open and confirm the precondition fails.
- Replay an approval token and confirm the executor rejects the second use.
- Remove a required field and confirm the renderer shows an error rather than applying a default.
- Deny approval, then confirm the agent cannot call the connector through another credential or tool.
Add recovery drills, not just unit tests. Pick a safe production-like action, execute it, invoke the recorded recovery handle, and verify downstream state. Measure whether operators can find the action by ID, identify the approver, reconstruct the rendered payload, and tell whether recovery completed. A control that exists only in documentation will fail precisely when the system is under pressure.
Review approval rates and overrides for design problems. Near-universal approval can mean the requests are routine enough for a narrower automatic policy, or it can mean reviewers have stopped reading. Frequent rejection for missing context means the action schema or rendering is deficient. Do not solve fatigue by training people to click faster. Remove low-consequence gates and improve the remaining ones.
Emergency access must leave stronger evidence
Emergency access may shorten the approval path, but it should never erase identity, scope, or the audit record. Define it before an incident: who can invoke it, which actions it permits, how long the capability lives, and who reviews its use afterward. A vague "break glass" administrator credential shared by a team is an untraceable bypass.
Use an individually authenticated request, a narrow expiring capability, a reason captured before execution, and immediate notification to someone other than the requester. Preserve the same canonical action payload and result envelope used in normal operation. If urgency makes prior approval impossible, require prompt retrospective review, but do not pretend that review was authorization. Name it accurately.
The system should fail closed when the approval service is unavailable for dangerous writes. Read work and isolated preparation can continue. Queued proposals can wait. Allowing the agent to commit because the control plane is down makes the control least effective during an outage, exactly when operators have less attention to spare.
Meaningful approval is deliberately narrow: one known actor, one canonical action, one current state, one bounded effect, and one recorded result. Put that contract at the executor, keep authority and evidence outside the agent's reach, and refuse automation where no honest recovery story exists.
FAQ
Where should a human approval step sit in an AI agent workflow?
Place it after the agent has resolved exact targets and parameters, but immediately before the trusted executor commits the external write. The executor must enforce the gate; a consent dialog beside an agent that already has production credentials is only decoration.
Should every agent tool call require approval?
No. Reads, calculations, dry runs, and isolated draft writes can usually proceed under narrow permissions. Require approval where a bounded proposal becomes an external side effect, and keep higher-risk operations manual.
What must an approval screen show?
Show the exact target, canonical parameters, proposed diff or effect, measured validation results, preconditions, and recovery method. Put tool-derived facts ahead of the agent's explanation, and expose defaults rather than hiding them.
How long should an agent approval remain valid?
Tie validity to the operation's risk and to a state precondition, not only to a clock. A base revision, ETag, record version, or schema version should invalidate the approval when the reviewed state changes.
What should be logged for each agent action?
Record actor and requester identity, action type, canonical intent, payload digest, policy version, approval, target preconditions, executor, attempts, observed result, target receipt, and recovery handle. Store proposal and outcome separately so investigators can see whether they diverged.
Is a chat transcript enough for an audit log?
No. It may explain the discussion, but it rarely captures normalized tool arguments, hidden defaults, exact target receipts, or the committed result. Keep it as supporting context beside a structured, protected action envelope.
What makes an automated change genuinely reversible?
You need a named inverse or compensating operation, the preserved state it requires, and a test showing recovery covers downstream effects. A rollback label without a working recovery handle is an aspiration, not a control.
Which operations should an AI agent never automate?
Do not let an agent expand its own authority, change its approval policy, erase its audit trail, destroy the last recovery copy, or make irrevocable legal or financial commitments. Some destructive actions may remain human-only; others should be forbidden through this path.
How should partial failures in an approved batch be handled?
Declare whether the batch is atomic before approval. For non-atomic work, log every target outcome and stop at an approved error threshold; never let the agent quietly widen the batch or continue through unexplained failures.
Does two-person approval make a dangerous agent action safe?
It helps with separation of duties, but it cannot fix an unreadable or incomplete action contract. Both reviewers can miss the same hidden parameter, so bind their decision to a clear canonical payload and current state.