codex exec: no way to allow MCP tool calls non-interactively without --dangerously-bypass-approvals-and-sandbox
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
codexv0.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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗