Agent Plugin mcp.json does not resolve ${VAR} env values for MCP servers
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?
- Install a plugin whose
mcp.jsonhasenv: { "DB_PASSWORD": "${DB_PASSWORD}" }. export DB_PASSWORD=secretin the shell.- Start a session and inspect the server (
codex mcp get <name> --json). - Observe the value is the literal
${DB_PASSWORD}, notsecret.
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.
4 Comments
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
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_placeholdersscans only for the literal strings${PLUGIN_ROOT}and${PLUGIN_DATA}; any other${VAR}inenvvalues (orargs) 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, containedcwd— so this looks like a policy decision surface rather than an oversight, which matters for which fix is right:${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 anenvvalue 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.${…}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 withenv: {"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.@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:
com.openaiextension: Allow the reservedcom.openaiextension 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.Agent Plugin
mcp.jsononly expands${PLUGIN_ROOT}and${PLUGIN_DATA}. Any other${VAR}is passed through literally.Method:
${DB_PASSWORD}/${HOME}style values in Agent Pluginenv..codex-plugin/.mcp.jsonenv_varslist and do not also ship a winning Agent Pluginplugin.jsonfor that server.~/.codex/config.tomlunder[mcp_servers.<name>.env].codex mcp get <name>that the child sees the real value, not the literal${VAR}.Evidence:
expand_agent_plugin_placeholdersis a two-entry whitelist. When both manifests exist, the Agent Plugin spec wins and the olderenv_varspath never runs.Independent community workaround; not an official OpenAI fix.