codex exec: no way to allow MCP tool calls non-interactively without --dangerously-bypass-approvals-and-sandbox

Open 💬 4 comments Opened May 22, 2026 by gsingal

Summary

In codex exec (non-interactive), MCP tool calls are auto-cancelled because stdin is closed and no config key I can find suppresses the approval prompt. The only working bypass is --dangerously-bypass-approvals-and-sandbox, which also disables -s read-only sandboxing. Asking: what's the right way to authorize MCP tool calls in codex exec while keeping -s read-only in place?

Environment

  • codex v0.130.0 (Homebrew, macOS arm64)
  • Auth: ChatGPT subscription via codex login

Repro

Minimal STDIO MCP server with one tool:

# /tmp/spike_mcp_server.py
from mcp.server.fastmcp import FastMCP
server = FastMCP("spike")
@server.tool()
def echo_hello(name: str) -> str:
    return f"hello {name}"
server.run()

Register it and call it from codex exec:

SPIKE_HOME=$(mktemp -d)
cp ~/.codex/auth.json "$SPIKE_HOME/"
codex --help >/dev/null  # ensure binary works

CODEX_HOME="$SPIKE_HOME" codex mcp add spike -- /opt/homebrew/bin/python3 /tmp/spike_mcp_server.py

# Verify registered
CODEX_HOME="$SPIKE_HOME" codex mcp list
# Name   Command                    Args                      Env  Cwd  Status   Auth
# spike  /opt/homebrew/bin/python3  /tmp/spike_mcp_server.py  -    -    enabled  Unsupported

# Call it
CODEX_HOME="$SPIKE_HOME" codex exec --color never \
  --skip-git-repo-check -s read-only \
  "Use the spike echo_hello tool with name=test and report what it returned." \
  </dev/null

Observed

mcp: spike/echo_hello started
mcp: spike/echo_hello (failed)
user cancelled MCP tool call

The MCP child process IS spawned (verified via RUST_LOG=info: MCP server stderr (/opt/homebrew/bin/python3): [spike_mcp_server] startup pid=...), but the tool call is auto-cancelled — presumably because stdin is closed so the approval prompt's reader gets EOF and codex interprets that as "user declined".

What I tried (none of these suppress the approval)

Tried each of these in $CODEX_HOME/config.toml:

approval_policy = "never"
default_tools_approval_mode = "never"
tools_require_approval = false
mcp_approval_policy = "never"
trusted_mcp_servers = ["spike"]

[projects."<spike-cwd>"]
trust_level = "trusted"

[mcp_servers.spike]
approval_policy = "never"

(approval_policy and default_tools_approval_mode are accepted by codex without errors — session header shows approval: never — but neither prevents the MCP-call cancellation. The other keys are silently ignored.)

I also tried --ignore-user-config + -c mcp_servers.spike.command=... CLI overrides; the MCP server didn't even register that way.

What does work

--dangerously-bypass-approvals-and-sandbox makes the call succeed:

mcp: spike/echo_hello (completed)
{"result":"hello test"}

But this flag also disables -s read-only, so it's not a viable production posture for a daemon that needs to keep filesystem reads sandboxed.

What I'm trying to do

I'm migrating a personal-AI daemon (codebase: github.com/gsingal/hanuman, issue #143) from claude -p to codex exec so it doesn't burn API credits. The general-agent layer needs to call our first-party STDIO MCP server (read-only Google tools + a frozen-URL allowlist enforced server-side in Python) while keeping -s read-only codex sandboxing so the model can't read arbitrary files outside the per-call scratch dir.

Ask

What's the supported config to authorize MCP tool calls in codex exec without --dangerously-bypass-approvals-and-sandbox?

Specifically: is there a way to say "trust this MCP server's tool calls without prompting" that works alongside -s read-only?

If this isn't currently supported, would you accept a config key proposal like [mcp_servers.<name>] auto_approve_tools = true (or a global default_mcp_tools_approval_mode = "never") for the same use case other agent-framework users will hit when calling codex non-interactively?

Why I think this matters beyond my use case

Any non-interactive use of codex exec with custom MCP servers (cron jobs, headless daemons, CI-driven agents, evaluation harnesses) hits this same wall. The --dangerously-bypass-approvals-and-sandbox flag's name (correctly) discourages using it in production, but right now it's the only path to a working non-interactive MCP-tool call.

Happy to provide more repro detail, RUST_LOG output, or test a candidate fix.

View original on GitHub ↗

4 Comments

jshaofa-ui · 3 months ago

Proposed Solution: MCP-Specific Approval Config for Non-Interactive Mode

Root Cause

MCP tool calls go through the same approval prompt as other tools. When stdin is closed (non-interactive), EOF is interpreted as "user declined". No config key bypasses MCP approval while keeping sandbox active.

Fix

  1. Add [mcp_servers.<name>] auto_approve_tools = true config key
  2. Respect approval_policy = "never" for MCP calls (currently only affects non-MCP)
  3. Split --dangerously-bypass-approvals-and-sandbox into --bypass-approvals + --bypass-sandbox
fn handle_mcp_tool_call(call, config) {
    let auto_approve = config.mcp_servers.get(&call.server_name)
        .map(|s| s.auto_approve_tools).unwrap_or(false);
    if auto_approve || config.approval_policy == Never {
        return execute_tool_call(call);
    }
    prompt_for_approval(call)
}

Full solution: solutions/codex-24135-mcp-tool-calls-non-interactive-fix.md

kayalopez · 3 months ago

I think there is one safety detail worth making explicit if Codex adds an MCP-specific non-interactive approval path.

For headless codex exec, I would avoid making this only a broad boolean like auto_approve_tools = true. The production-safe shape is closer to:

  • keep the filesystem/network sandbox setting independent from MCP approval
  • trust a server by stable identity: configured name, transport, command/url, cwd, and env-file boundary
  • trust individual tools by namespaced key, for example spike.echo_hello, not just raw tool name
  • store a reviewed discovery snapshot from tools/list
  • re-enter review if a tool is added, removed, or its metadata/schema changes
  • treat server annotations such as read-only/destructive/idempotent as advisory inputs, not proof
  • in non-interactive mode, fail closed if no reviewed allow entry matches

That would preserve the thing this issue is asking for: first-party MCP tools can run in codex exec -s read-only without stdin approval prompts, while the user does not have to use the broader dangerous bypass flag.

I made a small free helper for this exact review artifact while testing MCP launch/security workflows:

https://github.com/kayalopez/ai-agent-launch-tools/releases/tag/v0.1.7

It turns non-sensitive tools/list JSON into an allow / ask / deny matrix and emits a snapshot digest plus per-tool metadata digests. It can also print a Codex config.toml review snippet: allow -> approval_mode = "approve", ask -> approval_mode = "prompt", and deny -> disabled_tools, without changing sandbox settings. The useful bit for this issue is the acceptance-test pattern: run discovery, approve exact tool keys, then verify a changed tools/list snapshot forces review instead of inheriting approval silently.

No tool calls are executed by the helper; it only classifies pasted/exported metadata.

rpelevin · 2 months ago

For non-interactive codex exec, I would keep sandbox policy and MCP approval policy completely separate.

The unsafe shape is a broad switch that means "auto-approve MCP tools" and accidentally becomes equivalent to bypassing the sandbox. The safer shape is a scoped MCP approval policy that can be evaluated without stdin:

  • MCP server identity;
  • tool name;
  • canonical args digest or args pattern;
  • read/write/destructive classification;
  • allowed workspace or resource scope;
  • network scope, if relevant;
  • expiry;
  • policy id;
  • decision: allow, deny, require_interactive.

Then headless execution can work like this:

  • if a pending MCP call matches an explicit allow rule, execute under the existing sandbox;
  • if it matches deny, return a terminal denied tool result;
  • if it requires interactive approval and stdin is closed, fail closed with a clear approval-required result;
  • never make --dangerously-bypass-approvals-and-sandbox the only path to non-interactive MCP.

Regression tests I would want:

  • read-only sandbox remains active while an allowed MCP tool runs;
  • write/destructive tool is denied without a matching policy;
  • changed args do not reuse a prior allow decision;
  • closed stdin produces a visible approval-required result, not silent cancellation;
  • policy expiry invalidates future headless calls.

That gives automation a safe path without making approval bypass and sandbox bypass the same lever.

Brodin2001 · 1 day ago

This is exactly the kind of problem I'm trying to validate.

I've been building a small Python authorization layer that sits outside the agent and evaluates individual tool calls before execution, rather than having to choose between interactive approval and effectively bypassing the safety layer.

The interesting use case seems to be headless/CI agents where you still want deterministic permissions and possibly approval policies without a human sitting at stdin.

If you've still got the use case around, I'd be happy to give you the prototype to test against your MCP setup.

I'd be especially interested in whether it actually solves the gap or just adds another layer you wouldn't want in the stack.