Review-delegate MCP tool calls hang forever: approval elicitation is unanswerable in delegated sub-agents (forward_events has no ElicitationRequest arm)

Open 💬 1 comment Opened Jul 8, 2026 by martijnwalraven

What issue are you seeing?

Under codex exec review (and any review flow that delegates to a sub-Codex), the first call to an MCP tool that requires approval is announced in the transcript (mcp: <server>/<tool> started) but never dispatched: the JSON-RPC tools/call frame is never written to the server process's stdin, and codex waits indefinitely — the 300s default tool_timeout_sec never fires (observed 23 min on 0.142.5 and 15+ min on 0.143.0 before manual kill). Plain codex exec and interactive Codex dispatch the same tool against the same server fine (review-flow calls 0/5, elsewhere 16/16 on the same machine/server/day).

Because unannotated tools default to approval-required, this hits any local stdio MCP server whose tools don't declare annotations — which is most of them — on the first tool call of every review run.

Versions: codex-cli 0.142.5 and 0.143.0 (Homebrew cask), macOS aarch64 (Mac OS 26.5.2), model gpt-5.5. Code references below are at current main (f1affbac); the mechanism is unchanged since at least rust-v0.142.5. codex doctor clean (runtime brew 0.143.0, install consistent).

Root cause

Chain verified by a trace-logged repro and a discriminating config experiment on the stock binary:

  1. The review task runs as a delegated one-shot sub-Codex with approval_policy pinned to Never (core/src/tasks/review.rs:118, via codex_delegate.rs).
  2. MCP tools without annotations default to approval-required — requires_mcp_tool_approval(None) treats missing destructive_hint/open_world_hint as destructive/open-world (core/src/mcp_tool_call.rs:2163).
  3. mcp_permission_prompt_is_auto_approved does not auto-approve: policy is Never but the sandbox is not full-disk-write (codex-mcp/src/mcp/mod.rs:79).
  4. The in-session guardian route is off: it requires OnRequest/Granular, and the delegate is pinned to Never (core/src/guardian/review.rs:173).
  5. tool_call_mcp_elicitation (stable, default-enabled) sends the approval as EventMsg::ElicitationRequest and awaits a oneshot with no timeout: rx_response.await in request_mcp_server_elicitation (core/src/session/mcp.rs:280).
  6. forward_events in core/src/codex_delegate.rs intercepts and answers ExecApprovalRequest, ApplyPatchApprovalRequest, RequestPermissions, and RequestUserInput from the delegate — but has no arm for ElicitationRequest; it falls through the catch-all as a display event. The gap is acknowledged in the code (codex_delegate.rs:731): "TODO(ccunningham): Support delegated MCP approval elicitations here too … Today guardian only auto-reviews the RequestUserInput compatibility path for delegated MCP approvals." One-shot delegates additionally return a closed ops channel (codex_delegate.rs:262-266), so no Op::ResolveElicitation can ever reach the delegate session.
  7. McpToolCallBegin is emitted before the approval gate (mcp_tool_call.rs:227 vs :236), so the transcript says "started".
  8. tool_timeout_sec wraps only the rmcp service operation inside execute_mcp_tool_call, downstream of the approval gate — it never starts, so it never fires.

Why plain codex exec works: the exec main session runs approval: on-request with the auto-review guardian, so step 4 routes the approval to the guardian in-session and the call proceeds.

What steps can reproduce the bug?

  1. Register a local stdio MCP server whose tools carry no annotations, e.g.:

``toml
[mcp_servers.myserver]
command = "node"
args = ["my-mcp-server.js"]
``

  1. Run a review that calls one of its tools:

``
codex exec review "Review commit HEAD. Additionally, call the <tool> tool via MCP."
``

  1. Within ~a minute the transcript shows mcp: myserver/<tool> started and never completes; the server child's stdin has received exactly the initialize handshake + tools/list and nothing more.

Discriminating experiment (stock 0.143.0, no code changes): the same prompt wedges with default config (5/5 across 0.142.5–0.143.0) and completes in seconds when the approval gate is short-circuited with -c 'mcp_servers.<name>.default_tools_approval_mode="approve"'. A trace-logged wedge run (RUST_LOG=codex_mcp=trace,rmcp=trace,...) shows both the main and delegate sessions completing initialize+tools/list, the delegate's FunctionCall, the begin event — and zero rmcp trace activity after it: the call never reaches the rmcp client. Byte-level: live inspector autopsy of both spawned server processes of a wedged run — stdin bytesRead = initialize handshake + tools/list exactly, streams flowing and healthy, zero bytes after the handshake; children idle in kevent; the delegate rollout ends at the function_call entry with no output.

Annotating the server's tools (readOnlyHint: true, or destructiveHint: false + openWorldHint: false) also makes the wedge disappear, confirming the approval gate as the choke point — but servers shouldn't have to annotate to avoid an infinite hang.

What is the expected behavior?

A review-delegate MCP approval surfaces somewhere answerable (guardian or user) or resolves per policy — and tool_timeout_sec (or any bound) limits the wait in every case. Two defects as read: (1) delegated sub-agents emit MCP approval elicitations that nothing can answer; (2) an announced tool call can wait forever with no timeout.

Additional information — suggested minimal fix (patch available)

In run_codex_thread_interactive, disable Feature::ToolCallMcpElicitation on the delegate config, pinning delegated MCP approvals to the RequestUserInput compatibility path that forward_events already answers (guardian auto-review under exec; the parent's real user in interactive flows) — removable once the elicitation TODO is implemented. Regression test included: a review delegate calling an approval-requiring MCP tool must surface an answerable prompt on the parent session and complete once answered (it times out at the approval wait on unpatched source, passes with the pin).

A working implementation of exactly this shape — the pin, the regression test, and a managed-features caveat handled (an admin pin that force-enables the feature is kept and warned about) — is public at https://github.com/martijnwalraven/codex/tree/fix-delegated-mcp-approval-hang, shared as a fix outline per the contribution policy; happy to file it as a PR if invited. Caveats: the exec-review path is live-proven (patched binary runs the wedging repro to completion); the interactive-TUI-review path is analyzed at source but not live-tested; the pin applies to all delegated sub-agents, and a review-task-scoped variant is the more conservative alternative.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗