Codex CLI Environment Management: Turn-Scoped Selections, Profiles, Shell Policies, and Multi-Environment Workflows
Codex CLI Environment Management: Turn-Scoped Selections, Profiles, Shell Policies, and Multi-Environment Workflows
Professional software teams do not write code in a vacuum. Every meaningful project involves at least three environments — development, staging, and production — each with its own credentials, endpoints, feature flags, and infrastructure topology. Codex CLI has steadily accumulated the primitives needed to navigate this reality without leaking secrets or deploying to the wrong target. This article consolidates the environment management surface area as of v0.135.0, covering configuration profiles, shell environment policies, MCP per-server environment targeting, and the recently landed turn-scoped environment selections.
The Problem: Environment Sprawl Meets Agent Autonomy
When a human developer switches from staging to production, the cognitive overhead is low: change a .env symlink, swap a kubeconfig context, or toggle a shell alias. But an autonomous agent operating in full-auto mode has no such intuition. Without explicit guardrails, Codex will happily run terraform apply against whichever AWS account happens to be configured in the ambient shell. The consequences range from embarrassing (staging data in production logs) to catastrophic (production database migrations triggered by a test prompt).
Codex CLI addresses this with four layered mechanisms:
flowchart TD
A[Configuration Profiles] --> B[Shell Environment Policy]
B --> C[MCP Per-Server Env Targeting]
C --> D[Turn-Scoped Environment Selections]
A -->|"--profile staging"| E[Profile Overlay]
E --> F[Session Configuration]
D -->|"Per-turn env + cwd"| G[EnvironmentManager]
G --> F
Configuration Profiles
Profiles are named configuration layers stored as separate TOML files that overlay the base user configuration 1. When you pass --profile <name>, Codex loads ~/.codex/config.toml first, then merges ~/.codex/<name>.config.toml on top. Profile files only need to contain values that differ from the base.
Creating Environment-Specific Profiles
A typical setup for a team working across three environments:
# ~/.codex/staging.config.toml
model = "gpt-5.4-mini"
approval_policy = "on-request"
model_reasoning_effort = "medium"
[model_providers.staging-provider]
name = "Azure OpenAI Staging"
base_url = "https://staging.openai.azure.com/v1"
env_key = "AZURE_STAGING_API_KEY"
[shell_environment_policy]
inherit = "core"
set = { DEPLOY_ENV = "staging", AWS_PROFILE = "staging" }
exclude = ["PROD_*"]
# ~/.codex/production.config.toml
model = "gpt-5.5"
approval_policy = "untrusted"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
[model_providers.prod-provider]
name = "Azure OpenAI Production"
base_url = "https://production.openai.azure.com/v1"
env_key = "AZURE_PROD_API_KEY"
[shell_environment_policy]
inherit = "none"
set = { DEPLOY_ENV = "production", AWS_PROFILE = "production" }
include_only = ["PATH", "HOME", "USER", "DEPLOY_ENV", "AWS_PROFILE"]
Switching between environments becomes a single flag:
codex --profile staging "Run the integration test suite against the staging API"
codex --profile production "Check the health endpoint and report status"
Configuration Precedence
Profiles sit in the middle of Codex’s configuration hierarchy 1 2:
- Requirements (organisation-enforced, highest priority)
- Managed defaults
- CLI flags and
-coverrides - Project config (
.codex/config.toml, trusted projects only) - Profile files
- User config (
~/.codex/config.toml) - System config (
/etc/codex/config.toml) - Built-in defaults
This means a project-level config can override a profile, and CLI flags trump everything below requirements. The layering is intentional: it lets organisations enforce safety rails (via requirements) whilst giving individual developers flexibility through profiles 2.
Shell Environment Policy
The shell_environment_policy table controls which environment variables Codex forwards to any subprocess it spawns — every npm test, every terraform plan, every shell command the model proposes 3. This is the primary defence against secret leakage.
Policy Options
[shell_environment_policy]
# Baseline: "all" | "core" | "none"
inherit = "core"
# Explicit overrides injected into every subprocess
set = { NODE_ENV = "test", CI = "true" }
# Glob patterns for variables to exclude
exclude = ["*_SECRET", "*_TOKEN", "*_API_KEY", "AWS_SESSION_*"]
# Whitelist — when set, only matching variables survive
include_only = ["PATH", "HOME", "USER", "SHELL", "NODE_ENV", "CI"]
# Keep default KEY/SECRET/TOKEN filtering active
ignore_default_excludes = false
# Load user shell profile (.bashrc/.zshrc)
experimental_use_profile = true
Patterns use case-insensitive glob syntax (*, ?, [A-Z]) 3. The inherit = "core" baseline passes only PATH, HOME, USER, SHELL, and similar essentials — a sensible default that avoids the inherit = "all" footgun where every ambient credential flows through.
Environment-Specific Shell Policies
By combining profiles with shell policies, each environment gets its own variable boundary:
flowchart LR
subgraph Development
D1[inherit = all]
D2[exclude = PROD_*]
end
subgraph Staging
S1[inherit = core]
S2["set = {AWS_PROFILE = staging}"]
S3["exclude = [*_SECRET]"]
end
subgraph Production
P1[inherit = none]
P2["include_only = [PATH, HOME]"]
P3["set = {DEPLOY_ENV = production}"]
end
In development, you want maximal convenience — inherit everything, exclude only production credentials. In staging, inherit the core set and explicitly inject staging-specific variables. In production, start from nothing and whitelist only what the agent strictly needs.
MCP Per-Server Environment Targeting
Codex CLI v0.134.0 introduced per-server environment targeting for MCP servers 4. Prior to this release, all MCP servers in a session shared the same ambient environment. Now, each server can receive its own set of environment variables, with explicit control over whether variables are sourced locally or from a remote executor.
Configuration
[mcp_servers.staging-db]
command = "npx"
args = ["-y", "@supabase/mcp-server"]
env_vars = [
"SUPABASE_URL",
{ name = "SUPABASE_KEY", source = "local" },
{ name = "DATABASE_URL", source = "remote" }
]
[mcp_servers.staging-db.env]
SUPABASE_URL = "https://staging.supabase.co"
The env_vars array supports two forms 5:
- Plain strings (e.g.
"SUPABASE_URL") — read from Codex’s local environment, equivalent tosource = "local" - Objects with
source—source = "local"reads from the local shell;source = "remote"reads from the remote executor environment (requires remote MCP stdio)
For HTTP-based MCP servers (Streamable HTTP), authentication uses bearer tokens or environment-backed headers:
[mcp_servers.figma]
url = "https://mcp.figma.com/mcp"
bearer_token_env_var = "FIGMA_OAUTH_TOKEN"
[mcp_servers.internal-api]
url = "https://internal.example.com/mcp"
env_http_headers = { "X-Internal-Auth" = "INTERNAL_AUTH_TOKEN" }
OAuth-enabled servers can authenticate via codex mcp login <server-name>, with configurable callback ports and URLs 5:
mcp_oauth_callback_port = 5555
mcp_oauth_callback_url = "https://devbox.example.internal/callback"
Multi-Environment MCP Topology
A realistic configuration might wire different MCP servers to different environments:
# Development database — local credentials
[mcp_servers.dev-postgres]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres"]
env_vars = ["DEV_DATABASE_URL"]
# Staging monitoring — remote credentials
[mcp_servers.staging-datadog]
command = "npx"
args = ["-y", "@datadog/mcp-server"]
env_vars = [
{ name = "DD_API_KEY", source = "remote" },
{ name = "DD_SITE", source = "local" }
]
# Production — read-only, no write tools
[mcp_servers.prod-postgres]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres"]
env_vars = [{ name = "PROD_DATABASE_URL", source = "remote" }]
enabled = false # Disabled by default; enable via profile overlay
Turn-Scoped Environment Selections
The newest addition to the environment management story is turn-scoped environment selections, landed via PR #18416 6. This feature adds experimental turn/start.environments parameters that allow specifying a per-turn environment ID and working directory, resolved by an EnvironmentManager before turn processing begins.
What This Enables
Before turn-scoped selections, switching environments mid-session required restarting Codex with a different --profile flag or manually changing environment variables. Now, app-server sessions can manage multiple environments simultaneously and target a specific environment and working directory on each turn 6.
sequenceDiagram
participant Dev as Developer
participant Codex as Codex CLI
participant EM as EnvironmentManager
participant Env1 as Staging Env
participant Env2 as Production Env
Dev->>Codex: Turn 1: "Run tests" (env=staging, cwd=/app)
Codex->>EM: Resolve environment "staging"
EM->>Env1: Activate staging credentials + cwd
Env1-->>Codex: Environment ready
Codex-->>Dev: Tests pass on staging
Dev->>Codex: Turn 2: "Check health" (env=production, cwd=/ops)
Codex->>EM: Resolve environment "production"
EM->>Env2: Activate production credentials + cwd
Env2-->>Codex: Environment ready
Codex-->>Dev: Health check OK
This is particularly valuable for:
- Multi-workspace sessions — working across a frontend and backend repository in a single conversation, each with its own environment context
- Cross-environment validation — running a migration on staging, then immediately verifying the equivalent schema in production, without session restarts
- Remote setups — targeting different remote executors per turn, each with their own credential scopes
Current Status
Turn-scoped selections are experimental as of v0.135.0. The feature passes through the core protocol operations and is primarily consumed by app-server integrations and the SDK’s thread/turn APIs 6. CLI users benefit indirectly when the app-server manages their session, but the interactive TUI does not yet expose an environment picker. Expect this to stabilise over the next few releases.
Putting It All Together: A Multi-Environment Workflow
Here is a practical workflow combining all four mechanisms for a team managing a microservices platform:
1. Base Configuration
# ~/.codex/config.toml
model = "gpt-5.5"
approval_policy = "on-request"
[shell_environment_policy]
inherit = "core"
ignore_default_excludes = false
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
2. Profile Overlays
# ~/.codex/dev.config.toml
model = "gpt-5.4-mini"
approval_policy = "auto-edit"
[shell_environment_policy]
inherit = "all"
exclude = ["PROD_*", "*PRODUCTION*"]
set = { DEPLOY_ENV = "development" }
# ~/.codex/staging.config.toml
approval_policy = "on-request"
[shell_environment_policy]
inherit = "core"
set = { DEPLOY_ENV = "staging", AWS_PROFILE = "staging" }
exclude = ["PROD_*"]
[mcp_servers.staging-sentry]
command = "npx"
args = ["-y", "@sentry/mcp-server"]
env_vars = ["SENTRY_STAGING_DSN"]
3. Project-Level Overrides
# .codex/config.toml (in repo root, trusted project)
[mcp_servers.project-db]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres"]
env_vars = ["DATABASE_URL"]
4. Session Usage
# Morning development work — relaxed approvals, all env vars
codex --profile dev "Implement the new user registration endpoint"
# Pre-deploy validation — tighter controls
codex --profile staging "Run the full integration suite and report failures"
# Production incident — maximum safety
codex --profile production "Investigate the 500 errors in /api/orders, read-only"
5. SDK Turn-Scoped Targeting
For automated pipelines using the Python SDK, turn-scoped selections let a single script validate across environments without restarting the client:
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(
base_instructions="You are a deployment validator"
)
# Turn 1: validate staging
staging_result = thread.run(
input_text="Run smoke tests against the staging API"
# environment="staging" — experimental parameter
)
# Turn 2: validate production (same thread, different env)
prod_result = thread.run(
input_text="Check production health endpoints"
# environment="production" — experimental parameter
)
Security Considerations
Environment management is fundamentally a security concern. A few hard-won guidelines:
-
Never set
inherit = "all"in production profiles. Start from"none"and whitelist explicitly. The convenience of"all"is not worth the risk of leakingAWS_SECRET_ACCESS_KEYinto a model-proposed shell command. -
Use
excludepatterns defensively. The defaults filter variables containingKEY,SECRET, andTOKEN, but only whenignore_default_excludes = false3. Do not disable this unless you have a specific reason. -
Prefer
source = "remote"for production credentials. When MCP servers need production access, sourcing credentials from the remote executor keeps them off the developer’s local machine entirely 5. -
Use organisation requirements to enforce baselines. The
requirements.tomlmechanism sits at the top of the configuration precedence hierarchy and cannot be overridden by profiles, CLI flags, or project config 2. Use it to mandatesandbox_mode = "read-only"for production profiles. -
Scope MCP servers to profiles. Rather than enabling a production database MCP server globally, define it in the production profile and keep it
enabled = falsein the base config. This prevents accidental production queries during development sessions.
Citations
-
OpenAI, “Config basics – Codex”, https://developers.openai.com/codex/config-basic ↩ ↩2
-
OpenAI, “Advanced Configuration – Codex”, https://developers.openai.com/codex/config-advanced ↩ ↩2 ↩3
-
OpenAI, “Configuration Reference – Codex”, https://developers.openai.com/codex/config-reference ↩ ↩2 ↩3
-
OpenAI, “Codex CLI v0.134.0 Release Notes”, https://github.com/openai/codex/releases/tag/rust-v0.134.0 ↩
-
OpenAI, “Model Context Protocol – Codex”, https://developers.openai.com/codex/mcp ↩ ↩2 ↩3
-
starr-openai, “Add turn-scoped environment selections”, Pull Request #18416, https://github.com/openai/codex/pull/18416 ↩ ↩2 ↩3