Plugin manifest cannot declare user-supplied config, leaving credentials with no input path
A plugin whose MCP server needs a credential — an API key, a token, a client secret — has no way to ask the user for it, and the user has no way to supply it through Codex.
Where it breaks
Two independent gaps close off every path:
The manifest cannot declare a config input. RawPluginManifest accepts name, version, description, keywords, skills, mcp_servers, apps, hooks, and interface. There is no field for user-supplied values, so a plugin cannot state that it needs a secret, let alone have Codex prompt for one at install time.
User config cannot fill the gap either. PluginMcpServerConfig carries only enabled, default_tools_approval_mode, enabled_tools, and the tool deny-list. Its doc comment is explicit about why:
This intentionally excludes transport settings: plugin manifests own how the MCP server is launched, while user config owns enablement and tool policy.
So [plugins."name@marketplace".mcp_servers.server] in config.toml cannot add env or env_vars.
That leaves exactly one channel: env_vars forwarding from the host process environment, resolved in create_env_for_mcp_server via env::var_os. The plugin names the variables; the user must arrange for them to exist in the environment Codex itself was started in.
Why that channel is not sufficient
- It is invisible. Nothing in
codex plugin addor/pluginstells the user which variables to set. They have to read the plugin's README, or the manifest. - It fails silently by default. Install the plugin, restart, and the server dies on startup with whatever error it emits for missing config. Nothing connects that back to "you needed to export three variables."
- Shell config does not cover GUI launches.
~/.zshrconly applies to interactive shells;~/.zshenvcovers non-interactive ones too, but neither is read when Codex is launched from Spotlight or the Dock, because no shell is involved. Getting a variable to a GUI-launched Codex meanslaunchctl setenvor a LaunchAgent — which puts the secret in the environment of every GUI application on the machine. - It pushes secrets into plaintext. The natural place to put an
exportis a dotfile, which people back up and commit. Reading from the system keychain is possible but every plugin author has to document that dance themselves.
What Claude Code does
Its plugin manifest has a userConfig block: each entry declares type, title, description, and optionally sensitive and required. The client prompts at enable time, stores sensitive values in the OS keychain rather than settings, and substitutes them into the MCP server config as ${user_config.KEY}.
A plugin shipping the same MCP server to both clients ends up with two manifests that diverge on exactly this point — Claude Code prompts and stores in the keychain, Codex needs the user to edit a shell file and restart.
Suggestion
Let the manifest declare inputs, and let their values reach the MCP server config:
{
"userConfig": {
"api_token": {
"type": "string",
"title": "API token",
"description": "Found under Settings → Developer",
"sensitive": true,
"required": true
}
},
"mcpServers": {
"example": {
"command": "npx",
"args": ["-y", "example-mcp"],
"env": { "EXAMPLE_TOKEN": "${user_config.api_token}" }
}
}
}
Prompting could reuse the existing authentication: ON_INSTALL / ON_USE policy already present on marketplace entries. Storage for sensitive: true values ideally goes to the OS keychain; even config.toml would be an improvement over "figure out your own shell and launchd setup."
A narrower fix that would also help: allow env and env_vars in PluginMcpServerConfig, so a user can at least supply credentials in config.toml without touching their shell. That contradicts the current doc comment, but the boundary it draws — manifests own launching, user config owns policy — leaves credentials with no owner at all.
Context
Hit this shipping a WeCom MCP server as a plugin to both clients. The Claude Code side is a dialog with three fields. The Codex side is a README section explaining ~/.zshenv, security find-generic-password, and why the variables have to exist before Codex starts.
Versions: codex-cli 0.146.0, source read at main.