Approval timeouts keep old approvals from doing new work
Approval timeouts stop old deploy, delete, and SSH approvals from executing after targets or conditions change. Learn how to bind, expire, and recheck them.

An approval is permission for one proposed action at one moment. It is not a coupon that an agent can redeem whenever it becomes convenient.
That sounds obvious until an agent queues a production deploy, a deletion, or an SSH command while a human is busy. The reviewer sees a reasonable request, approves it, then the world moves. A newer build wins the queue. The target set changes. Someone repoints an environment alias. An incident changes the safety conditions. If the old approval can still fire, the system has converted a decision about yesterday's state into authority over today's state.
Approval timeouts fix only one part of that problem, but it is a part teams often leave open. Put a short, hard expiry on approvals for actions whose meaning can change. Then bind the approval to the exact proposed action and recheck the conditions that can change before execution. Do both. Either control on its own leaves a gap.
An approval has a freshness budget
Every approval spends time while it waits to execute. The more the target can move, the smaller that budget should be.
A request to restart a disposable preview worker may stay meaningful for half an hour. A request to promote a release to production can become stale in a few minutes if another release, rollback, or incident response can change the correct next move. A command that deletes a named backup snapshot should expire quickly when the snapshot list is under active retention work. An SSH command against a mutable host alias deserves the shortest window of the lot.
The bad default is to choose one large number because it avoids annoying people. Eight hours sounds accommodating. In practice, it lets approvals accumulate during lunch, overnight, or across a shift handoff. A reviewer can approve a deploy at 10:02, forget it, and discover at 16:40 that a much later queue state consumed their click. The approval interface looked careful. The execution path was careless.
Set the window by asking a narrower question: how long can this exact proposed action remain an accurate description of what will happen?
For most teams, a useful starting policy looks like this:
- Production deploy or rollback: 5 to 15 minutes.
- Destructive deletion: 2 to 10 minutes, depending on whether the object set is fixed.
- SSH command with write effects: 2 to 5 minutes.
- Read-only SSH inspection: no per-action approval, or a longer window if your environment still requires one.
- Routine nonproduction changes with a pinned build: 15 to 30 minutes.
Those are operating defaults, not universal constants. A five-minute expiry is too long if an automation loop can alter the target every few seconds. It is too short if the action requires an on-call reviewer to gather evidence first. The right response is not to quietly lengthen the deadline forever. Give the reviewer the context they need and make the request easy to regenerate from current state.
GitHub Actions makes an important distinction here: a deployment job can wait for required review, and a pending job that receives approval can then proceed and gain access to its environment secrets. GitHub also documents that an unapproved job can fail after 30 days. That protects queues from living forever, but 30 days is not a useful freshness window for an approval about a changing deployment. Your own gate should treat approval age as part of authorization, not as queue housekeeping.
Bind the decision to the action, not to a label
An approval must cover concrete action data. "Approve production deploy" is a label. It tells a reviewer almost nothing about the object that will run.
For a deploy, bind the decision to the immutable artifact digest or commit identifier, the destination, the migration plan if one exists, the configuration revision, and the release operation. For a delete, bind it to an immutable object list or a snapshot of the query result, plus the deletion mode. For SSH, bind it to the verified host identity, user, command template, expanded arguments, working directory if relevant, and a bounded description of input files.
This is the distinction teams blur most often:
- Approval of an intent means the reviewer agrees with a broad objective, such as "remove obsolete previews."
- Approval of an action means the reviewer agrees to this executor deleting these object identifiers, with this command, before this deadline.
Intent approval has a place in change management. It cannot substitute for an execution approval when an agent can act on a live system. If you approve an intent and then let the agent resolve targets later, you have delegated the important part of the decision to the agent.
OWASP's Transaction Authorization Cheat Sheet calls this out in a different domain. It says the person authorizing a transaction needs to identify and acknowledge significant transaction data, and it warns that changing data after authorization creates a time-of-check to time-of-use failure. The example is financial, but the rule transfers cleanly: approval data must be protected against modification, and a data change should invalidate the existing authorization.
A digest gives the executor something precise to compare. Do not hash a vague natural-language summary and call it done. Canonicalize the fields that control the effect, serialize them deterministically, then hash that canonical form.
{
"request_id": "appr_01JX...",
"action_type": "deploy",
"action_digest": "sha256:8b1d...",
"summary": {
"artifact": "registry.example/app@sha256:4fa2...",
"environment": "production",
"operation": "promote",
"config_revision": "7c0e...",
"migration": "none"
},
"issued_at": "2026-07-22T14:03:00Z",
"expires_at": "2026-07-22T14:13:00Z",
"status": "pending"
}
The summary is for the human. The action_digest is for the executor. Keep both. Humans need to see useful facts; services need an exact equality check. If the agent changes even one bound field, it must submit a new request and obtain a new decision.
Expiry and invalidation solve different failures
A deadline prevents old approvals from lingering. Invalidation removes an approval as soon as relevant facts change. You need both because waiting for a timer is sloppy when the system already knows the request no longer matches reality.
Invalidate a pending approval when the action digest changes. That rule is nonnegotiable. Also invalidate when a dependency that affects meaning changes: a deployment's intended environment revision, a selected deletion set, a host key, a release lock holder, or a required change ticket state.
Do not invalidate on every unrelated event. If any log line, unrelated commit, or harmless metric shift kills an approval, reviewers learn that requests are unreliable and start approving without inspecting them. The rule should track facts that alter either the requested effect or the safety preconditions.
Use three states, not two:
pendingmeans the exact request may still be approved before its deadline.approvedmeans a reviewer approved it, but the executor has not consumed it.consumedmeans execution claimed the approval exactly once.
Add terminal states for expired, invalidated, rejected, and failed. A rejected request should not become pending again because an agent retried a network call. A failed execution should not silently reuse the same approval unless you can prove the action never started and no relevant state changed. In most action systems, making the agent ask again is safer and easier to explain.
The executor should make these checks in one transaction or one atomic compare-and-set operation:
if now >= expires_at: reject as expired
if status != approved: reject as unavailable
if stored_digest != supplied_digest: reject as changed
if live_preconditions fail: reject as stale
atomically change status from approved to consumed
execute the action
Do not mark the approval consumed after the action starts. Two workers can race, both observe approved, and both execute. Consume first with an atomic state transition, then record that execution began. If the process dies after consumption, treat the outcome as unknown until the executor can establish whether it reached the target. That is inconvenient. Duplicate destructive work is worse.
Deploy approvals must follow the artifact
A deployment request becomes stale when its artifact, destination, release plan, or order in the queue changes. Branch names and moving tags are not enough.
The deploy prompt should identify an immutable artifact. That might be an image digest, a signed release bundle hash, or an immutable build record. It should also tell the reviewer whether the executor will run database migrations, change feature configuration, restart instances, or replace a prior deployment. These details affect the approval. Hiding them behind a generic "deploy" button invites rubber stamping.
A solid deploy approval contract includes:
- The immutable build identifier and source revision.
- The exact target environment and account or cluster identity.
- The release operation, such as promote, rollback, or redeploy.
- The configuration and migration revisions that will apply.
- A concurrency token or deployment generation.
The concurrency token matters when a later change supersedes an earlier request. Suppose build A waits for approval. Build B finishes, passes checks, and becomes the release you now intend to ship. If build A's request remains valid, an approving reviewer may accidentally release the old build. When build B enters the same release lane, invalidate build A's pending approval. Do not depend on reviewers to notice timestamps in a busy queue.
GitHub's deployment documentation separates environment protection from workflow concurrency. Its concurrency controls can cancel pending work in a group, while environment approval controls whether a job may proceed. That separation is useful: a queue policy can decide which run is current, while an approval gate can decide whether that exact current run may execute. Combining them carelessly produces the classic failure where the right person approves the wrong run.
The executor must recheck before it deploys. A practical preflight might verify that the requested artifact still exists, the environment still maps to the expected target identity, no newer release holds the lane, and the migration plan still matches the approved digest. If any check fails, mark the request invalidated and show the reviewer a fresh request. Never silently substitute build B because build A was approved. That is a different action.
Avoid a popular but weak pattern: approving a pull request and treating that approval as production authorization. Code review answers whether a proposed change belongs in the codebase. It does not answer whether this build should run in production now, after the current incident, with the current migrations and target state. Keep those decisions separate.
Delete prompts need a frozen object set
Deletion approvals become dangerous when the request contains a query instead of the resolved objects. "Delete backups older than 30 days" can describe a different set every minute.
At request creation, resolve the query to object identifiers and record a snapshot marker. The approval screen should show the count, a few representative identifiers, the retention basis, and the exact deletion mode. The executor should use that frozen set, not rerun the broad query after approval.
If the set is too large to show in full, give the reviewer a stable manifest identifier and a concise breakdown. Do not reduce the prompt to "Delete 8,421 items" with no boundary. A count cannot tell the reviewer whether the list includes the wrong tenant, current backups, or an unexpected prefix.
Consider this request:
{
"action_type": "delete_objects",
"scope": "archive/preview/",
"selection": {
"manifest_digest": "sha256:19e7...",
"object_count": 184,
"newest_object_at": "2026-06-19T03:11:00Z",
"oldest_object_at": "2025-11-02T18:24:00Z"
},
"mode": "permanent",
"expires_at": "2026-07-22T14:08:00Z"
}
At execution, confirm that the manifest still exists and that every object identifier still resolves to the expected version. If the storage system supports versioning, bind deletion to versions rather than names. Names can be reused. A new object written at the same path after approval must not inherit the old object's death sentence.
A short deadline is especially important when the request is based on age or a live inventory. The longer the request waits, the more likely a newly eligible object, a restored item, or a reclassified record changes the intended set. If the system cannot freeze the set, it should not allow a single approval to authorize a broad deletion query. Ask for a narrow, recent request instead.
Soft deletion and permanent deletion deserve different prompts and different expiry windows. Soft deletion may be reversible, but do not use reversibility as an excuse for vague approvals. Recovery is often slow, incomplete, or dependent on permissions that the person asking for deletion does not control.
SSH approvals decay faster than people think
SSH is unusually sensitive to stale context because names, sessions, environment variables, and working trees can change underneath the same command text.
systemctl restart api looks specific until you ask which machine receives it, what api points to there, which deployment is currently active, and whether a later incident changed the reason for restarting anything. rm -rf /srv/tmp/job-123 can be safe against one host and catastrophic against another if an alias, mount, or shell expansion changes.
For an SSH action, approve a structured command request, not a terminal transcript. The request should include the host's verified identity, the target user, a fixed command template, fully expanded allowed arguments, the declared working directory, and any expected input digest. If the agent needs a shell, constrain the shell command to an explicit payload rather than authorizing an open interactive session.
A reasonable approval card might read:
Host: prod-api-03, host key SHA256:K4f...
User: deploy
Command: /usr/local/bin/release-health --release 2026.07.22.4 --repair-cache
Directory: /srv/api
Effect: writes cache state, may restart one service
Expires: 14:08 UTC
That is still not proof that the command is safe. It is enough information for a human to recognize what they are authorizing. The executor then reconnects, verifies the host identity again, confirms the command digest, and runs it before the deadline.
Never let approval of a command on prod-api authorize execution after DNS, inventory, or a bastion mapping resolves that label to a different host. Bind to the host's cryptographic identity where the connection setup allows it. If a legitimate host-key rotation occurs while approval waits, invalidate the request. That can feel noisy during maintenance. It is preferable to approving a command for one machine and sending it to another.
Read-only commands deserve their own category. Teams often force approval for every SSH call because they have one control and apply it everywhere. The result is approval fatigue, then reviewers click through commands they cannot parse. Separate harmless inspections from actions that write, restart, alter access, or expose sensitive output. Use per-call approval where the command's effects warrant it, and keep the prompt compact enough to read.
The approval screen must make change visible
A precise backend contract is wasted if the reviewer sees only a sentence written by the agent. The screen needs to show the fields that could make the answer change.
Lead with the effect: deploy this digest to this environment; permanently delete this fixed manifest; run this command on this verified host. Put the deadline where a reviewer will notice it before approving, and show the time in an unambiguous timezone. Show a countdown only as a convenience. The executor's server-side timestamp decides validity.
When a request changes, do not replace the old contents in place and leave the approval button enabled. Mark it invalidated. Create a new request with a visible explanation such as "artifact changed" or "target inventory changed." A reviewer who approved an earlier version should have to make a fresh decision. That extra click is the point.
NIST SP 800-63B-4 describes authentication intent as user intervention that confirms a claimant intends to authenticate or reauthenticate. Action approval needs the same discipline, but more narrowly. A finger press or confirmation click should express intent for the displayed operation, not general willingness to let an agent continue.
Avoid prompts that turn time pressure into a trick. A two-minute window for a complex deletion forces a reviewer to choose between blind approval and expiry. The request should be ready to inspect before it reaches the reviewer. Use a short execution deadline after the reviewer has adequate context, not a rushed decision deadline that punishes careful reading.
A comment field can help when the reviewer needs to state why an unusual action is acceptable. Do not make a comment mandatory for ordinary work. Mandatory boilerplate produces text nobody reads. Require it for overrides, exceptional expiry extensions, or actions that exceed a defined blast radius.
The executor owns enforcement
The system that holds authority to perform the action must enforce expiry, binding, and one-time consumption. A workflow UI, chat bot, or agent framework can request approval, but it cannot be the final judge if another component can bypass it.
That is why an action gateway is a useful boundary. The agent proposes an HTTP call or SSH command. The gateway evaluates whether the request has a current approval, injects the credential if appropriate, performs the operation, and returns the result. The agent never needs a reusable credential that would let it bypass the approval path later.
Sallyport follows this shape for HTTP APIs and SSH: the agent connects through its MCP shim while credentials remain in the app's encrypted vault, and the app performs the action rather than handing secrets to the agent. Its per-call keys are a natural place to require a fresh confirmation for actions where context changes quickly. The timeout and action-binding logic still need to be explicit in the request path; a confirmation without those checks can age into the same problem.
Keep approval state adjacent to the executor, or make it cryptographically verifiable by the executor. A signed approval token can work if it includes the request identifier, action digest, issue time, expiry, reviewer identity, and nonce. The executor must still check revocation and consume the nonce once. A signed token that stays valid after a request was invalidated is simply a well-signed stale approval.
Treat clocks carefully. Use a trusted service clock for expiry decisions, store timestamps in UTC, and reject approvals at or after the expiry instant. The agent's local clock and the browser's countdown are display aids. They are not authorization inputs.
Logs should explain why execution did or did not happen
When an approval expires, teams need a record that says more than "denied." They need to know whether a reviewer never responded, a request changed after approval, the executor found a failed precondition, or another worker already consumed the approval.
Write an immutable event trail that connects the proposal, displayed summary, approval decision, invalidation or expiry, preflight checks, execution attempt, and result. Store the action digest in every event. If the request was regenerated, record the replacement request identifier without implying that the earlier approval carried over.
A useful event shape looks like this:
{
"event": "approval.invalidated",
"request_id": "appr_01JX...",
"action_digest": "sha256:8b1d...",
"reason": "release_lane_superseded",
"replaced_by": "appr_01JY...",
"recorded_at": "2026-07-22T14:06:22Z"
}
Record attempts to consume expired approvals as well. They reveal agents that retry blindly, workers with bad clocks, and UI paths that failed to refresh. They also let an incident reviewer distinguish an expired request from an action that actually reached production.
Sallyport's Sessions journal and Activity journal are projected from an encrypted, hash-chained audit log, and its sp audit verify command checks that chain offline over ciphertext. That kind of trail is useful only if the event vocabulary is honest. Include expiry, invalidation, and failed preflight events, not just successful calls that make the dashboard look clean.
Do not confuse auditability with prevention. A perfect record of an old approval running against new state is evidence of a failure. The prevention check has to run before the executor sends the request or opens the SSH channel.
Make expired requests cheap to replace
Short windows work only when a new request is easier than arguing with an old one. If regeneration requires reentering a ticket number, rebuilding a command by hand, and chasing three people, teams will pressure you to lengthen every timeout until it is meaningless.
The agent should be able to resubmit from fresh state, but it must show the new facts plainly. If only the expiry changed and every bound field remains identical, a new request can preserve the same human-readable summary while receiving a new identifier and deadline. If any action field or live precondition changed, say what changed. Do not make the reviewer compare opaque hashes.
Keep an expiry extension as an exception. If you offer one, require the executor to rerun all preflight checks and require the reviewer to see the current summary again. An "extend for 30 minutes" button that renews the old token is an approval bypass with nicer typography.
Start by measuring four things: how often approvals expire, how often approved actions are invalidated before execution, how long requests wait, and which action types generate repeated retries. Those results tell you whether the deadline is too short, the queue is slow, or the agent creates requests before it has stable inputs.
A stale approval should fail quietly and specifically: the executor rejects it, the log names the reason, and the agent requests a current decision if the work still makes sense. That small refusal is how human control stays attached to the action that actually happens.
FAQ
What is an approval timeout?
An approval timeout is a hard deadline after which an unconsumed approval cannot authorize an action. It prevents someone from approving a request, walking away, and having that old decision applied after the target, command, or surrounding conditions have changed.
How long should a deployment approval last?
A deploy approval should usually expire in minutes, not hours. Use a shorter window when the release can be superseded quickly or when an incident is active; use a modestly longer window only when the exact build and target remain pinned and rechecked at execution.
Does an approval expiry prevent stale actions by itself?
No. A short expiry limits how long a decision can sit unattended, but it does not prove that the action still means the same thing. Bind the approval to an immutable action digest and revalidate live preconditions immediately before execution.
What should a deletion approval include?
The approval must name the exact object or selection, its version or snapshot marker, the intended effect, and the expiry. If a delete request says only "remove old logs," it is not specific enough to approve safely.
Should SSH command approvals expire?
Use a short expiry, a fixed command template, an exact host identity, and a fresh connection or host check before the command runs. Do not let an approval for a command on one host turn into permission for the same text on whichever host later occupies an alias.
Are approval timeouts the same as idempotency?
A timeout addresses staleness. Idempotency addresses duplicate execution. A deployment can have both problems, so use an approval deadline and an execution idempotency key or release record that makes a second run harmless or rejects it.
What happens when an approval expires?
Expire the approval and create a new request from the current action data. Do not offer a generic "extend" button, because it trains reviewers to renew a decision without seeing what changed.
Is an approval timeout the same as a wait timer?
No. Waiting periods and expiry windows solve opposite problems. A wait timer delays eligibility to run; an expiry deadline limits how long an approval remains valid after a reviewer has made a decision.
Where should approval expiration be enforced?
Approval expiry belongs at the action gateway or executor, where the system can compare the approved digest to the request it is about to run. A chat interface may display the deadline, but it cannot be the final authority.
What should an audit log record for an expired approval?
The audit record should preserve the action digest, the human-readable summary, reviewer identity, decision time, expiry, execution time, live checks, and outcome. Record expired and invalidated attempts too, because they explain why an action did not run.