Hooks: let PermissionRequest answer concurrently with the native approval prompt (first answer wins)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What variant of Codex are you using?
Codex CLI (codex-cli 0.147.0) with codex app-server.
What feature would you like to see?
A way for a PermissionRequest hook to answer alongside the native approval prompt rather than before it — that is, a hook can say "show the native prompt now; I may still answer; whichever answer arrives first wins."
Today the event is serial by construction:
//! This event runs in the approval path, before guardian or user approval UI is
//! shown.
// codex-rs/hooks/src/events/permission_request.rs
run_permission_request_hooks(...) is awaited before the approval flow continues (core/src/tools/orchestrator.rs:434), and a handler's timeout has a floor but no ceiling (hooks/src/engine/discovery.rs:485 — timeout_sec.unwrap_or(600).max(1)). So a hook that waits on a human somewhere else holds the entire approval: the person sitting at the terminal sees a pending hook row and has nothing they can answer until the hook returns.
That leaves an external approval tool with only bad options:
- short hook timeout — the remote answerer usually loses, so the feature barely works;
- long hook timeout — the local user is locked out of their own approval for as long as the hook waits.
Either shape would solve it:
- A hook output that keeps the request open:
{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "defer_and_watch"}}}
Codex shows the native prompt immediately, the handler stays alive, and the first decision to arrive — hook or human — wins.
- Or a declaration flag in
hooks.json, e.g."concurrent": true, with the same semantics.
The half that matters most either way is cancellation: when the local user answers first, the handler needs to learn that (a signal, or a line on stdin) so an external approval card can be withdrawn instead of dangling until it times out.
Why this matters
The app-server already has exactly this property, which is why external tooling gravitates there: a pending approval ServerRequest reaches subscribed connections and is replayed to new ones (app-server/src/outgoing_message.rs:352, replay_requests_to_connection_for_thread), so another client can answer while the TUI shows the same prompt, first answer wins.
But the app-server is an internal, unversioned surface, and it carries only what it carries. Hooks are the declarative, documented, cross-tool surface — and PermissionRequest there already covers strictly more in a single event: apply_patch, shell, unified_exec, sandbox escalation (core/src/tools/runtimes/shell/unix_escalation.rs), MCP tool calls (core/src/mcp_tool_call.rs), and network policy (core/src/tools/network_approval.rs).
For a tool that puts approvals on a phone while the terminal stays usable, the only reason not to build on hooks today is that they cannot be concurrent.
Related, but different
- #23465 — expose the effective approval reviewer, or an explicit defer, so an external UI does not steal approvals Codex would have handled itself.
- #28833 — tell a passive notifier whether a human is actually required.
Both concern the routing and observability of a request that is still answered serially. This issue is about the concurrency of the answer: even when the reviewer really is user and a human really is required, a hook can only answer instead of the terminal, never with it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with codex-rs/hooks/src/events/permission_request.rs and the await in core/src/tools/orchestrator.rs, then inspect the timeout handling in hooks/src/engine/discovery.rs. Compare the existing request replay and response flow in app-server/src/outgoing_message.rs. Done means a PermissionRequest hook and the native prompt can remain active together, the first answer wins, and the losing handler is notified or cancelled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- authorization
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100