Agent Plugin mcp.json does not resolve ${VAR} env values for MCP servers

Open 💬 4 comments Opened Aug 13, 2026 by wangauone

What version of Codex CLI is running?

0.147.0

What subscription do you have?

n/a (not model-dependent)

Which model were you using?

n/a (not model-dependent)

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

Problem

In the Agent Plugin mcp.json format, env values written as ${VAR} are not resolved — the MCP server receives the literal string ${VAR} instead of the value from the environment. (Only ${PLUGIN_ROOT}/${PLUGIN_DATA} expand.)

// Agent Plugin mcp.json
{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
  "mcpServers": {
    "my-server": {
      "type": "stdio",
      "command": "...",
      "args": ["..."],
      "env": { "DB_PASSWORD": "${DB_PASSWORD}" }
    }
  }
}
// server receives:  DB_PASSWORD=${DB_PASSWORD}   (literal, not resolved)

The old .codex-plugin format handled this via env_vars, which forwarded the named variable from the environment:

// .codex-plugin/.mcp.json
{
  "mcpServers": {
    "my-server": {
      "command": "...",
      "args": ["..."],
      "env_vars": ["DB_PASSWORD"]
    }
  }
}
// server receives:  DB_PASSWORD=<value from environment>

The Agent Plugin format has no equivalent (env_vars is rejected), so there is no way to pass user config (IDs, credentials) to a plugin's MCP server. Verified via codex mcp get and a probe server that dumps its received env.

Why it matters

The same ${VAR} env works on Claude Code (userConfig), which inject values, so migrating a plugin to the Agent Plugin format is a Codex-only regression. Hardcoding literals isn't viable (per-user, secret). Related: #36854, #24401.

What steps can reproduce the bug?

  1. Install a plugin whose mcp.json has env: { "DB_PASSWORD": "${DB_PASSWORD}" }.
  2. export DB_PASSWORD=secret in the shell.
  3. Start a session and inspect the server (codex mcp get <name> --json).
  4. Observe the value is the literal ${DB_PASSWORD}, not secret.

What is the expected behavior?

${VAR} in Agent Plugin env values is resolved against the user environment at launch, restoring the capability the old .codex-plugin env_vars config provided.

View original on GitHub ↗

4 Comments

wangauone · 10 days ago

More critical findings

When a plugin ships both an Agent Plugin spec manifest (plugin.json) and a .codex-plugin/ config, Codex prioritizes the Agent Plugin spec (find_plugin_manifest_path). But the Agent Plugin spec in Codex has a feature gap, it doesn't forward env variables, while the more comprehensive .codex-plugin/ config, which does, never takes effect.

So when env forwarding is essential, plugin authors must give up the Agent Plugin spec, and with it, compatibility with the other harnesses that rely on it.

Possible solutions can include

jdcodes1 · 10 days ago

Confirmed on main @ 1f41cc5d92 — the expansion is a two-entry whitelist, and everything else passes through byte-for-byte:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/codex-mcp/src/agent_plugin_config.rs#L374-L386

expand_agent_plugin_placeholders scans only for the literal strings ${PLUGIN_ROOT} and ${PLUGIN_DATA}; any other ${VAR} in env values (or args) is left verbatim, so the spawned server receives the literal ${VAR} — your report exactly. The surrounding normalization (normalize_agent_plugin_stdio_server, #L150-L235) shows the format is deliberately curated — bare-or-contained commands only, reserved-variable protection, contained cwd — so this looks like a policy decision surface rather than an oversight, which matters for which fix is right:

  1. If ${VAR} should resolve (as the Agent Plugins schema implies): note the security consequence before implementing it naively — a third-party plugin manifest interpolating arbitrary host environment variables into a process it controls is an env-exfiltration channel (${AWS_SECRET_ACCESS_KEY} in an env value ships the secret to the plugin's server). Codex's native MCP config handles this class with explicit reference fields (bearer_token_env_var, env_http_headers) rather than free interpolation; an equivalent here would be resolving ${VAR} only for variables the user allowlists (per-plugin config), not whatever the manifest names.
  1. If it's intentionally unsupported, the loader should reject unknown ${…} placeholders at manifest load with a validation error — today the server launches with a ${VAR} literal where a credential was expected, and the resulting failure surfaces as an opaque server-side auth error far from the cause. Silent passthrough is strictly worse than either supporting or refusing.

Either resolution also deserves a line in the Agent Plugins format docs, since the schema's examples are what set the ${VAR} expectation. Test shape: manifest with env: {"TOKEN": "${MY_TOKEN}"} → assert (1) resolved-and-allowlisted, or (2) load-time validation error naming the placeholder — never a literal ${MY_TOKEN} in the child environment.

helloeve · 9 days ago

@jdcodes1

As you pointed out, the current version of the Agent Plugin spec intentionally limits expansion to ${PLUGIN_ROOT} and ${PLUGIN_DATA}. However, this creates a significant feature gap between Codex's native plugin manifest and the Agent Plugin manifest, as there is currently no way to forward environment variables to MCP servers in the latter.

Before this feature gap is formally resolved in the Agent Plugin spec (e.g., via a proper allowlist mechanism), how should we handle plugins that need this functionality? Would Codex be open to either of these approaches:

  1. Adjust the manifest resolution logic: Have Codex favor its own native .codex-plugin/ manifest over the Agent Plugin spec when both exist in a repository.
  2. Leverage the com.openai extension: Allow the reserved com.openai extension block to accept an mcpServers override. This would let plugin providers use the existing env_vars allowlist pattern to pass environment variables to the MCP server.
naipi11 · 9 days ago

Agent Plugin mcp.json only expands ${PLUGIN_ROOT} and ${PLUGIN_DATA}. Any other ${VAR} is passed through literally.

Method:

  1. Do not put ${DB_PASSWORD} / ${HOME} style values in Agent Plugin env.
  2. If you need host env forwarding, ship the legacy .codex-plugin/.mcp.json env_vars list and do not also ship a winning Agent Plugin plugin.json for that server.
  3. Otherwise set the concrete value in user ~/.codex/config.toml under [mcp_servers.<name>.env].
  4. Verify with codex mcp get <name> that the child sees the real value, not the literal ${VAR}.

Evidence: expand_agent_plugin_placeholders is a two-entry whitelist. When both manifests exist, the Agent Plugin spec wins and the older env_vars path never runs.

Independent community workaround; not an official OpenAI fix.