Codex prints MCP tool call requires approval, but approval policy is never when a call to an MCP server's tool needs approval and your approval policy forbids asking. Codex has no one to ask, so it refuses. The fix is one line per server in ~/.codex/config.toml: default_tools_approval_mode = "approve". It pre-approves that server's tools and leaves the shell sandbox exactly as strict as it was.
We hit this while connecting Codex to a local HTTP MCP server, and measured it on codex-cli 0.150.1 in September 2026. The rest of this post covers when it appears, what each sandbox mode does, why the fix is not the same as bypassing the sandbox, how to point Codex at an HTTP MCP server, and one debugging trap that looks exactly like a dead server.
Why does "never" mean refuse instead of "don't ask"?#
Codex's approval policy decides whether Codex may stop and ask a person. Its CLI help describes never as "Never ask for user approval". It does not say "approve everything".
So whenever Codex decides something needs approval, never leaves one outcome: the action is refused and the failure goes back to the model. MCP tool calls are where most people run into this. With no approval mode set on the server, Codex treats the call as something a person should confirm. Under never, that confirmation cannot happen, so the call fails with the sentence in the title.
Two open GitHub issues show the same confusion from different sides, as of September 2026:
- openai/codex #24135:
codex exec(v0.130.0) with a custom MCP server. Calls came back as "user cancelled MCP tool call" because the approval prompt read from a closed stdin. The reporter tried several keys, includingdefault_tools_approval_mode = "never", which is not one of the documented values. Only--dangerously-bypass-approvals-and-sandboxworked, and they asked for a way to approve MCP tools while keeping-s read-only. - openai/codex #19554: in the Codex app, the "Never" policy is described as "Run without asking for approval". Computer Use actions were denied with no prompt. The reporter suggests the copy should say that actions needing approval are blocked.
The per-server setting below is that way to approve MCP tools while keeping the sandbox.
Where does this error show up?#
Anywhere Codex runs without a person watching the terminal:
codex execin scripts, cron jobs and git hooks.- CI pipelines, where there is no TTY and no one to press "yes".
- The Codex SDK, where
approvalPolicyis an option you set up front, so an app that pinsneverhas no one answering prompts. - Apps that drive Codex for their users, such as desktop agent apps and internal tools. They usually pin
never, becauseon-requestwould park the turn waiting for an answer the app has no channel to send.
In each case the symptom is the same. The model can see the tool, it tries to call it, the call fails, and the model either apologises or works around the missing tool. Nothing crashes, which is why it can go unnoticed for a while.
What happens under each sandbox mode?#
Here is what we measured on codex-cli 0.150.1, calling a tool on a streamable HTTP MCP server with approval_policy set to never:
| Sandbox mode | No default_tools_approval_mode | default_tools_approval_mode = "approve" |
|---|---|---|
read-only | Refused: "MCP tool call requires approval, but approval policy is never" | Call runs |
workspace-write | Refused, same message | Call runs |
danger-full-access | Call runs | Call runs |
The middle column is the trap. The tools work in the one mode that removes the sandbox, and fail in the two modes most people use. If you only tested with full access, everything looked fine. Then you tightened the sandbox for production and every tool call started failing.
The trap also invites the wrong fix. "It works under full access" pushes people toward danger-full-access or the bypass flag, which gives the model an unsandboxed shell just to call a tool that was never the risky part.
How do I fix it in config.toml?#
Set the approval mode on each MCP server whose tools you trust to run unattended:
# ~/.codex/config.toml
approval_policy = "never"
sandbox_mode = "workspace-write"
# A stdio server Codex starts as a process
[mcp_servers.docs]
command = "npx"
args = ["-y", "your-docs-mcp-server"]
default_tools_approval_mode = "approve"
# A streamable HTTP server Codex connects to
[mcp_servers.internal]
url = "http://127.0.0.1:8787/mcp"
bearer_token_env_var = "INTERNAL_MCP_TOKEN"
default_tools_approval_mode = "approve"
For a one-off run, pass the same keys with -c. Values are parsed as TOML, so strings need their quotes:
codex exec --sandbox read-only \
-c 'mcp_servers.docs.default_tools_approval_mode="approve"' \
"Look up the retry policy in the docs server and summarise it"
codex exec does not take the top-level -a/--ask-for-approval flag, so set approval_policy in config or with -c approval_policy="never".
What are the other approval modes?#
Codex's MCP documentation lists four values for default_tools_approval_mode, and the same four for a per-tool override at tools.<tool>.approval_mode:
| Value | What it does |
|---|---|
auto | The docs describe it as automatic approval. We did not test it separately; in our test, leaving the key unset produced the error. |
prompt | Always asks a person. With never, expect a refusal. |
writes | Asks only for tools that modify data. |
approve | Pre-approved. Runs without asking. |
writes is worth a look if your server marks which tools modify data. Under never the read tools should run and the rest should be refused. Test that against your own server before relying on it.
To keep one dangerous tool away from an unattended run, don't rely on an approval mode being refused. Remove the tool:
[mcp_servers.internal]
url = "http://127.0.0.1:8787/mcp"
default_tools_approval_mode = "approve"
disabled_tools = ["delete_record"]
enabled_tools is the allowlist form, and disabled_tools is applied after it. A tool the model cannot see is safer than one it can see and will keep trying to call.
Is this the same as --dangerously-bypass-approvals-and-sandbox?#
No, and the difference is the reason to use the per-server setting.
default_tools_approval_mode = "approve" | --dangerously-bypass-approvals-and-sandbox | |
|---|---|---|
| Scope | One MCP server's tools | Everything in the run |
| Shell commands | Still sandboxed (read-only stays read-only) | Run with no sandbox |
| Approval prompts | Skipped for that server's tools | Skipped for everything |
| Codex's own warning | None | "EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed" |
An MCP tool runs inside the MCP server, not inside Codex's shell sandbox. The sandbox never governed what your tool does. It governs what the model's shell commands can touch. Approving the tools changes nothing about the shell. Bypassing the sandbox changes everything about it.
So "approve" is a statement about the server: "I wrote this, or I trust whoever did, and its tools are allowed to run." If you would not make that statement, the answer is writes, a shorter enabled_tools list, or a person at the keyboard. It is not a bigger hammer.
How do I connect Codex to an HTTP MCP server with a url?#
An MCP server does not have to be a process Codex spawns. Give the entry a url instead of a command and Codex treats it as a streamable HTTP server:
| Key | Transport | Purpose |
|---|---|---|
command, args, env | stdio | Codex starts the server as a child process |
url | streamable HTTP | The server's address |
bearer_token_env_var | streamable HTTP | Name of an environment variable holding a token for the Authorization header |
http_headers | streamable HTTP | Static headers |
startup_timeout_sec | both | How long to wait for the server to start. Default 10 |
tool_timeout_sec | both | How long a single tool call may run. Default 60 |
enabled | both | false turns the server off without deleting it |
From the terminal, codex mcp add internal --url http://127.0.0.1:8787/mcp --bearer-token-env-var INTERNAL_MCP_TOKEN writes the same entry.
A URL is the right choice when the tools already live inside a running program: an Electron app, a dev server, a long-lived daemon holding a database connection. Wrapping those in a stdio process means a second copy of the state, or a small proxy that exists only to forward calls. A local HTTP endpoint lets Codex call the same objects your program already uses.
If you do this on a laptop, two rules matter:
- Bind to
127.0.0.1and require a token. Every process running as your user can reach loopback, so "local" does not mean "only mine". Pass the token by environment variable name, which is whatbearer_token_env_varis for. Process listings show arguments, not environment values. - Build a fresh MCP server object per request if you use the TypeScript SDK's streamable HTTP transport statelessly. An
McpServerconnects to one transport at a time. When we shared one instance across two connections, replies to the second went to the first, which looked like a hung tool.
Why does my MCP server look dead when it isn't?#
Once the approval fix is in, the next failure is quieter. Codex runs, the model says it's done, and your tool was never called. No error.
The cause we hit was in our own test harness. It started the HTTP MCP server and then ran codex exec with Node's spawnSync. spawnSync blocks the Node event loop until the child exits, and the server lived in that same process. So the server could not accept Codex's connection, the handshake timed out after startup_timeout_sec, and Codex carried on without the server and without an error. The model answered "DONE" having called nothing.
Three checks catch this:
- Read stderr as well as stdout.
codex execwrites the final message to stdout and its progress, including MCP tool calls and any refusal, to stderr. A script that reads only stdout sees a confident answer and cannot tell whether a tool ran. - Compare token counts. Tool definitions are part of the prompt. In our case a run that had loaded the MCP tools used 14,315 tokens and the broken run used 5,915. A large, unexplained drop means the tools never arrived.
- Never block the thread that serves the tools. If the MCP server and the code that launches Codex share a process, use async
spawn, or run the server in a separate process. Raisingstartup_timeout_secdoes not help, because a blocked loop stays blocked. Settingrequired = trueon the server makescodex execexit with an error when the server fails to start, instead of carrying on without it.
Keep a negative control, too: run the same call once without default_tools_approval_mode and assert that it is refused. If a Codex release changes the default, or someone deletes the line because it looks decorative, that test tells you before your users do.
How does Universe give Codex the same tools as Claude?#
Universe is a Mac app that runs agents through the model account you already pay for, including Codex on a ChatGPT Plus, Pro or Business plan. A copy of Codex ships inside the app, so there is nothing separate to install.
Universe's own tools, such as browser control and typed artifacts, also work when Codex runs the turn. They reach Codex through a local bridge, and every engine gets the same instructions. More on running several engines side by side is in Running Claude Code, Codex, Gemini and Grok from one app.
Where this doesn't help: a turn running on a different Mac has the model's native tools but not Universe's own. And none of this changes Codex's limits or your ChatGPT plan's usage. Universe doesn't resell model usage (see pricing).
If you are building your own harness around codex exec, you don't need Universe for any of this. The config above is the whole fix. If you would rather have the bridge, the browser and the typed artifacts already set up, download Universe. It needs macOS 13 or later, on Apple silicon or Intel.
The short version#
- The error means Codex wanted approval for an MCP tool and
neverforbids asking, so it refused. - Add
default_tools_approval_mode = "approve"to each trusted[mcp_servers.<name>]entry. Usewritesordisabled_toolsfor servers you only partly trust. - It does not loosen the shell sandbox. Do not reach for
danger-full-accessor the bypass flag to make tools work. - HTTP servers use
urlplusbearer_token_env_var. They don't need to be processes. - If a tool silently never runs, check stderr, compare token counts, and make sure nothing is blocking the thread that serves the tools.
Related: if a CLI agent is refusing to resume a conversation rather than refusing a tool, see Claude Code deletes your conversations after 30 days. Codex also keeps its history in your home directory, under ~/.codex/sessions. For sharing one skill across Codex and the other agents, see One SKILL.md, four agents.
Questions#
- What does "MCP tool call requires approval, but approval policy is never" mean?
- Codex decided the MCP tool call needs a person's approval, and your approval policy says Codex may never ask. It cannot ask, so it refuses the call. The tool result comes back as that sentence and the model sees a failed tool. "Never" means never prompt, not always allow. Set default_tools_approval_mode = "approve" on the server to pre-approve its tools.
- Does default_tools_approval_mode = "approve" turn off the Codex sandbox?
- No. It only pre-approves calls to that one MCP server's tools. The sandbox still controls what shell commands the model runs can read and write, so read-only stays read-only. That is the difference from --dangerously-bypass-approvals-and-sandbox, which removes both the approval prompts and the sandbox for every command in the run.
- Why do MCP tools work with danger-full-access but not with read-only or workspace-write?
- When we tested codex-cli 0.150.1 in September 2026, MCP calls with no approval mode set were refused under read-only and workspace-write, and went through under danger-full-access. Full access removes the case where Codex wants approval. Switching to full access to get tools working gives the model an unsandboxed shell, so set the per-server approval mode instead.
- How do I connect Codex to an HTTP MCP server instead of a command?
- Give the server a url instead of a command in ~/.codex/config.toml, under [mcp_servers.<name>]. Codex treats it as a streamable HTTP server. Add bearer_token_env_var with the name of an environment variable that holds the token, or http_headers for static headers. From the terminal, codex mcp add <name> --url <url> writes the same entry.
- Why does codex exec answer without calling my MCP tool, with no error?
- The server probably never finished its handshake. Codex waits startup_timeout_sec (10 seconds by default) for a server, then carries on without it unless the server is marked required = true. Check stderr, where codex exec streams its progress, and compare token counts. A run that loaded the tool definitions uses noticeably more input tokens than one that did not.