Blog

Codex CLI and remote MCP servers: running unattended with a Bearer key

Aperisa ยท 8 min read

We pointed codex exec at a Streamable HTTP MCP server with two tools that return the same fixed value. In our test with Codex CLI 0.162.1, Codex called the first, refused the second, and exited 0. The only difference between the tools was one annotation, readOnlyHint: true.

That is MCP tool approval in Codex CLI 0.162.1 when nobody is there to approve. By default, codex exec runs with the approval policy set to never, and the default per-server mode lets through only tools annotated read-only, or non-destructive and closed-world. Connecting the server with a Bearer key is the easy part, and it's covered first below. Our view: decide approval per server in config.toml, mark the server required, and don't turn off the sandbox to get MCP calls through.

Checked on 2026-10-27. Tested on Codex CLI 0.162.1 (released October 9, 2026) on macOS 27.0.1, against a throwaway server on localhost with a fake token. The current stable release is 0.166.0 (checked October 29, 2026). We haven't rerun the test on it.

This post covers the Codex CLI, not Codex running in ChatGPT. OpenAI's MCP docs say the CLI, IDE extension and ChatGPT desktop app share config.toml on the same machine, and that ChatGPT web "doesn't read local Codex configuration files" and reaches remote MCP tools through plugins. The plugin auth docs describe OAuth or anonymous servers, and say ChatGPT cannot "present custom API keys." For plugins, that leaves OAuth or anonymous access, so for a static Bearer key, use the CLI.

How do you add a remote MCP server to Codex CLI?

One table in ~/.codex/config.toml, or .codex/config.toml in a trusted project:

[mcp_servers.records]
url = "https://your-server.example/mcp"
bearer_token_env_var = "MCP_TOKEN"

Or from the shell:

codex mcp add records --url https://your-server.example/mcp --bearer-token-env-var MCP_TOKEN
codex mcp list

Codex reads the variable at startup and sends Authorization: Bearer <value>. Put the variable's name in the config, not the token. In our test with Codex CLI 0.162.1, an inline bearer_token field failed to load (our test server entry was named x):

bearer_token is not supported for streamable_http
in `mcp_servers.x`

The other auth fields for Streamable HTTP servers, per the docs and present in the 0.162.1 source:

  • http_headers: static header values.
  • env_http_headers: header names mapped to environment variable names.
  • http_headers_helper: a local command that prints a JSON object of headers, useful for tokens that rotate. The docs say explicit bearer tokens take precedence over a helper's Authorization header.

Useful per-server fields: enabled_tools (allow list), disabled_tools (applied after it), startup_timeout_sec, tool_timeout_sec and required. The docs give the timeout defaults as 10 and 60 seconds, but the 0.162.1 source uses 30 and 300. Set both explicitly if they matter.

Why does codex exec refuse to call an MCP tool?

Because in codex exec nobody can approve, so by default Codex denies. The 0.162.1 source is explicit: "Default to never ask for approvals in headless mode." That is the default only. With --approve-for-me, which uses automatic review, exec rebuilds the policy. The non-interactive docs add that exec runs in a read-only sandbox by default.

Each MCP server then has an approval mode, default_tools_approval_mode, with per-tool overrides under tools.<tool>.approval_mode. In mcp_tool_call.rs at 0.162.1, the four values decide whether a call needs approval:

Mode Needs approval when
auto (default) The tool is annotated destructiveHint: true, or lacks both readOnlyHint: true and the pair destructiveHint: false plus openWorldHint: false
prompt Always
writes The tool isn't annotated readOnlyHint: true
approve Never

If a call needs approval and the policy is never, Codex denies it. The MCP spec makes readOnlyHint optional with a default of false, so a server that doesn't annotate its tools gets every call denied in exec under the default settings.

Our test server had two tools, lookup_annotated with readOnlyHint: true and lookup_plain with no annotations. In our test with Codex CLI 0.162.1, with default settings, the output included:

approval: never
sandbox: read-only
mcp: t/lookup_annotated (completed)
mcp: t/lookup_plain (failed)
MCP tool call requires approval, but approval policy is never

Exit code: 0. Our server's log showed no request for lookup_plain, so Codex denied the call before it left the machine. In a second run, adding default_tools_approval_mode = "approve" to the server entry made lookup_plain complete, with the sandbox still read-only.

Why the sandbox bypass seems to fix it

In the 0.162.1 source, MCP prompts are auto-approved when the policy is never and the sandbox is off or has full disk write access. So --sandbox danger-full-access or --dangerously-bypass-approvals-and-sandbox does unblock MCP calls. It also gives every shell command the model runs write access to your whole disk. We read this in the source and didn't test it. Set the approval mode on the server instead.

The docs also list --approve-for-me, which routes approval requests through automatic review with the workspace-write sandbox. We haven't tested how it treats MCP calls.

How do you run Codex unattended with a Bearer key?

Config first. Approve only the server you trust, and only the tools you need:

[mcp_servers.records]
url = "https://your-server.example/mcp"
bearer_token_env_var = "MCP_TOKEN"
required = true
enabled_tools = ["search", "get_record"]
default_tools_approval_mode = "approve"

[shell_environment_policy]
exclude = ["MCP_TOKEN"]

The last table matters because the run below puts MCP_TOKEN in Codex's whole environment. In the 0.162.1 source, the default shell environment policy passes the full environment to shell commands the model runs, so without the exclude line those commands can read the token. We read this in the source and haven't tested it.

required = true matters more than it looks. In our test with Codex CLI 0.162.1, with the token variable (TEST_MCP_TOKEN) unset and required off, Codex printed no warning on stdout or stderr and sent no request to the server. The model reported that the tool wasn't available, and exec exited 0. With required = true, the same run exited 1:

required MCP servers failed to initialize: t: Environment variable TEST_MCP_TOKEN for MCP server 't' is not set

Then the run. Load the token from your OS keychain or secret manager for this one process, rather than a dotfile:

MCP_TOKEN="$(security find-generic-password -s mcp-records -w)" \
  codex exec --sandbox read-only --ephemeral --json \
  -o last-message.txt \
  "Summarize this week's open items using the records tools." > run.jsonl

security is the macOS keychain CLI. Swap in your secret manager's equivalent. If you authenticate Codex itself with an API key, the docs say that in workflows that run repository-controlled code, you should set CODEX_API_KEY for the single run, not as a job-level variable.

Last, check the result. In our test with Codex CLI 0.162.1, a denied tool call left the exit code at 0, so look for it in the JSONL. The event looked like this (shortened):

{"type":"item.completed","item":{"type":"mcp_tool_call","server":"t","tool":"lookup_plain","error":{"message":"MCP tool call requires approval, but approval policy is never"},"status":"failed"}}
if jq -e 'select(.item.type? == "mcp_tool_call" and .item.status == "failed")' run.jsonl > /dev/null; then
  echo "an MCP tool call failed" >&2
  exit 1
fi

Where does Aperisa fit?

Aperisa is a remote MCP server. Your data is available to any harness that connects over Streamable HTTP with a Bearer key: OpenClaw, Hermes, Cursor, Codex, Antigravity, n8n or your own code. Give each agent its own key and set what it can reach: which data areas, work or personal, and how much detail, from full records down to totals only. Aperisa enforces those limits on the server.

Approve the server, keep the sandbox

In our test with Codex CLI 0.162.1, unattended Codex failed quietly in two places: a missing token removed a server's tools without a warning, and an unannotated tool was denied while exec exited 0. Mark the server required, set default_tools_approval_mode on the servers you trust, keep the token out of the shell environment, keep the sandbox read-only, and check the JSONL. If you're new to the server side, start with what an MCP server is. For pointing several agents at one data source, see one record, many agents.

Aperisa connects as a remote MCP server over Streamable HTTP with a Bearer key, and each key can be limited by data area and level of detail. Try it out.

FAQ

How do I add a remote MCP server with a Bearer token to Codex CLI? Add a [mcp_servers.<name>] table to ~/.codex/config.toml with url and bearer_token_env_var, or run codex mcp add <name> --url <url> --bearer-token-env-var <VAR>. Codex reads the token from that environment variable and sends it as Authorization: Bearer. In our test with Codex CLI 0.162.1, an inline bearer_token field was rejected.

Why does codex exec refuse to call an MCP tool? By default, codex exec runs with approval policy never. In our test with Codex CLI 0.162.1, a tool without a read-only annotation was denied with "MCP tool call requires approval, but approval policy is never", and setting default_tools_approval_mode = "approve" on the server let it run. In the 0.162.1 source, the default mode, auto, only skips approval for tools annotated read-only, or non-destructive and closed-world.

Does ChatGPT-hosted Codex support Bearer tokens for MCP servers? Not through plugins. This post covers the Codex CLI. ChatGPT web reaches remote MCP servers through plugins, and OpenAI's plugin auth docs describe OAuth or anonymous servers and say ChatGPT cannot present custom API keys.

What happens if the token's environment variable isn't set? In our test with Codex CLI 0.162.1, Codex printed no warning, the model said the server's tool wasn't available, and codex exec exited 0. With required = true on the server entry, codex exec exited 1 with "Environment variable ... is not set".

Sources

Questions

How do I add a remote MCP server with a Bearer token to Codex CLI?

Add a [mcp_servers.<name>] table to ~/.codex/config.toml with url and bearer_token_env_var, or run codex mcp add <name> --url <url> --bearer-token-env-var <VAR>. Codex reads the token from that environment variable and sends it as Authorization: Bearer. In our test with Codex CLI 0.162.1, an inline bearer_token field was rejected.

Why does codex exec refuse to call an MCP tool?

By default, codex exec runs with approval policy never. In our test with Codex CLI 0.162.1, a tool without a read-only annotation was denied with 'MCP tool call requires approval, but approval policy is never', and setting default_tools_approval_mode = "approve" on the server let it run. In the 0.162.1 source, the default mode, auto, only skips approval for tools annotated read-only, or non-destructive and closed-world.

Does ChatGPT-hosted Codex support Bearer tokens for MCP servers?

Not through plugins. This post covers the Codex CLI. ChatGPT web reaches remote MCP servers through plugins, and OpenAI's plugin auth docs describe OAuth or anonymous servers and say ChatGPT cannot present custom API keys.

What happens if the token's environment variable isn't set?

In our test with Codex CLI 0.162.1, Codex printed no warning, the model said the server's tool wasn't available, and codex exec exited 0. With required = true on the server entry, codex exec exited 1 with 'Environment variable ... is not set'.

Published

Keep reading