Treat approval prompts as state: one per endpoint, withdrawn when the request ends, bound to their turn, and denied instead of shown when nobody is there to answer.
Two releases, one idea
On 6 November 2026 two coding agents shipped changes to the same part of their design: the approval prompt. The OpenAI Codex CLI release 0.167.0 lists, among its fixes, that network approval prompts now close when their requests end, and that concurrent requests to the same endpoint prompt one at a time. The underlying pull requests, merged on 30 October, describe more: tracking each network prompt's lifetime and withdrawing it when the request disappears, validating a decision against the turn that raised the prompt so a stale answer cannot interrupt a later turn or persist a policy change, and queuing manual prompts by environment, host, protocol and port so that a one-time decision lets the next request prompt while a session grant covers the rest. The same release documents managed rules that require a fresh human decision even when saved approvals or hooks would allow a command.
Claude Code 2.1.319, published the same day, added a permissions.denyInsteadOfPrompting setting: a tool call that would wait on a permission prompt is denied and the turn continues, in terminal sessions, subagents and teammates.
Read together, these describe an approval prompt as something with a lifecycle rather than a dialog box. It is created by a request, belongs to a turn, can be superseded, withdrawn or answered, and has to behave sensibly when nobody is watching. This guide turns that into a design you can apply to any agent that asks people for permission, using an original model of an approval broker. It does not reproduce either tool's code, and nothing below describes their internals beyond what the release notes and pull request descriptions say.
A prompt is state
Picture the simplest implementation: when a tool call needs permission, the agent pauses and the interface shows a question. The person clicks Allow or Deny, and the answer goes back to whatever is waiting. That works for one request at a time from one turn, with a person always present. Each of the release-note items exists because one of those assumptions fails.
The broker in the companion keeps, for each endpoint key (environment, host, protocol, port), at most one visible prompt and a queue of waiting requests. Each prompt has an id, records the request and the turn that raised it, and ends in one of three ways: answered, withdrawn, or covered by a session rule.
Concurrency: one prompt per key
A coding agent installing dependencies may open three connections to registry.npmjs.org at once. A naive implementation shows three identical prompts. The person answers the first, and the other two are either answered by reflex, which trains people to click without reading, or left behind the first one, where they pile up.
The broker queues requests by key. In the model, three requests to the registry and one to catalog.example.test produce exactly two prompts: one for the first registry request and one for the catalog. What happens next depends on the decision:
- Allow once: the first registry request proceeds, and the next queued registry request now raises its own prompt. A one-time decision means one request.
- Allow for the session: the first request proceeds, the broker records a session grant for that key, and the two waiting registry requests resolve without a prompt.
- Deny for the session: the same, with a deny rule.
Keying by endpoint rather than by command string is a design choice worth copying. Two different commands that reach the same host raise the same question about network access; the person should answer it once per key, not once per spelling.
Withdrawal: the request ended, so the question is gone
Requests end without anyone answering: the command times out, the user interrupts the turn, the process making the connection exits. If the prompt stays on screen, a person can still answer it, and the answer then applies to nothing, or worse, to something else. The Codex pull request names the hazard precisely: a late cancellation could interrupt the turn even though the request no longer needed approval.
In the broker, request_ended removes the prompt if it is visible or the request if it is queued, marks the request withdrawn, and promotes the next waiting request for that key. A decision that arrives for a withdrawn prompt is rejected. The model's test checks the important consequence: a late "allow for the session" on a withdrawn prompt does not create a session grant. A stale click should never become a standing policy.
def decide(self, prompt_id, decision, current_turn):
live = [(k, r) for k, (p, r) in self.active.items() if p == prompt_id]
if not live:
return False # withdrawn or already answered
key, req = live[0]
if req.turn != current_turn:
return False # belongs to an earlier turn
del self.active[key]
self.results[req.rid] = "allowed" if decision.startswith("allow") else "denied"
if decision == "allow-session":
self.session_grants.add(key)
if decision == "deny-session":
self.session_denies.add(key)
self._advance(key) # next waiting request for this key
return TrueBinding: an answer belongs to a prompt and a turn
Agents run work in the background. A command started in turn 4 may still be making network requests in turn 6. If an approval raised by that background work is shown in turn 6, the person might reasonably think it is about what they just asked for. The Codex description distinguishes network approvals that require the originating turn from proxy approvals that a later active turn can service, and validates the originating turn before applying a decision.
The broker's rule is the strict version: a decision is applied only if it names a live prompt and arrives in the turn that raised it. Anything else is rejected and logged. In an interface, show the turn and the originating command next to every prompt, so the person can see what they are approving.
Unattended runs: deny and continue
When nobody is watching, such as a scheduled job, a CI run, or a subagent working while its parent waits, a prompt that waits for a person blocks forever or until a timeout, and the work that was blocked is often unrelated to the request that needs approval. Claude Code's denyInsteadOfPrompting takes the other branch: deny immediately and let the turn continue, so the agent can report that it lacked permission and do whatever else it can.
In the broker, unattended mode denies every request that would need a prompt and records why, while existing session grants and deny rules still apply. That last part matters: an unattended run with a pre-approved, narrow set of grants is useful; an unattended run that prompts is not.
Deny-and-continue is not free. The agent will now attempt alternatives, and those must be bounded by the same rules: a denied network fetch should not lead to the agent trying another host that is allowed by a broad rule. Give unattended runs the narrowest grants that let them finish, and make "denied for lack of approval" a visible outcome in the run's summary rather than a line buried in logs.
Fresh decisions for the commands that need them
The other half of the Codex release is the opposite of convenience. Managed rules can mark a command prefix as requiring a fresh, one-time human decision that overrides saved allowances, hook allowances and automatic review, and the pull request rejects such commands when no client capable of showing the prompt is connected. For high-risk commands, a session grant is the wrong tool: you want the person to see this specific invocation.
Combine the two ideas into one policy table. For each class of action, decide one of four behaviours: allow, deny, prompt with session grants allowed, or prompt for a fresh decision every time. Then decide what each behaviour becomes when unattended: allow and deny stay as they are, both prompt behaviours become deny.
What stays deterministic
The model may decide to make a request. It must not decide whether the request was approved, which turn a prompt belongs to, whether a prompt is still live, or what a session grant covers. Those are broker state, and the broker should be small enough to test exhaustively. The companion's seven tests cover one prompt per endpoint, next-request prompting after a one-time decision, session grants resolving waiting requests, withdrawal followed by a rejected late decision, a decision from the wrong turn, unattended denial, and grants still applying when unattended.
Checklist for an existing agent
- Prompts carry an id, the originating turn and the request they guard.
- Concurrent requests for the same key share one visible prompt.
- A request that ends withdraws its prompt; late answers are rejected and never persist as grants.
- Decisions are validated against the originating turn.
- Unattended mode denies instead of prompting, keeps existing grants, and reports denials in the run summary.
- High-risk actions require a fresh decision every time, and fail closed when no one can be asked.
Run the example yourself
The approval broker with per-endpoint queues, withdrawal, turn binding, session rules and unattended denial, and seven tests. Companion code on GitHub; its README lists the prerequisites, exact commands, expected output and limits.