Skip to content

Advanced Codex setups: providers, sandboxes and audit trails

An advanced Codex setup treats the Codex CLI as infrastructure: model traffic goes through a gateway, each job type gets its own reasoning effort, the sandbox boundary is tested before the agent meets it, and every unattended run leaves a machine-checkable record in JSONL events and OpenTelemetry.

Everything here was checked against Codex CLI 0.157.1. This page is for the developer or tech lead who already runs Codex daily and now has to answer harder questions. Security wants model traffic to go through the company gateway. CI runs Codex at 2 a.m. and nobody can say which commands it executed or which files it touched. A teammate copied a config snippet from an old blog post, and now Codex refuses to start.

What does an infrastructure-grade Codex setup give you?

Section titled “What does an infrastructure-grade Codex setup give you?”
  • A provider block that routes Codex through a proxy, Azure OpenAI or Ollama, and a check that proves the route works.
  • Per-job profiles (--profile ci, --profile deep) that set model effort and verbosity instead of one global compromise.
  • A sandbox boundary you tested with codex sandbox before an unattended run hit it.
  • Two jq checks that turn a run’s JSONL event stream into a pass/fail gate: failed commands and files touched outside the allowed paths.
  • An OpenTelemetry export to your own collector, so the audit trail does not depend on anyone reading the transcript.

In what order should you harden a Codex setup?

Section titled “In what order should you harden a Codex setup?”

Order matters: each step gives the next one something to check against.

  1. Make the config fail loudly. Run codex exec --strict-config … in CI so a key this Codex version does not recognise stops the run instead of being ignored.
  2. Fix the provider. Decide where model traffic goes (OpenAI directly, a gateway, Azure or a local model) before you tune anything that depends on the model.
  3. Test the sandbox. Run the commands your jobs need through codex sandbox and confirm what is allowed and what is denied.
  4. Split effort by job. Put CI, everyday and deep-reasoning settings into separate profiles.
  5. Record every run. Capture --json output and export OpenTelemetry events, then gate on them.

Configuration basics (file locations, precedence, approval modes) live in Codex configuration. This page starts where that one stops.

How do you route Codex through a gateway, Azure or a local model?

Section titled “How do you route Codex through a gateway, Azure or a local model?”

Point the built-in OpenAI provider at a gateway

Section titled “Point the built-in OpenAI provider at a gateway”

To keep the built-in openai provider and change only its endpoint, set openai_base_url in your user config (~/.codex/config.toml) or pass it for one run:

Terminal window
# Terminal: one run through a regional or corporate endpoint
codex -c openai_base_url='"https://llm-gateway.internal.example.com/v1"' "Summarise the open TODOs in src/"

Codex ignores openai_base_url in a project’s .codex/config.toml, so a cloned repository cannot redirect your traffic. The same project-local deny list in 0.157.1 covers model_provider, model_providers, notify and otel, so every block on this page belongs in your user config or in admin-managed config, not in the repository.

Define a custom provider with its own retry budget

Section titled “Define a custom provider with its own retry budget”

A gateway that needs its own key or retry policy gets a named provider. Built-in IDs are reserved. openai, ollama and lmstudio cannot be redefined under [model_providers]: Codex 0.157.1 refuses to start with “Built-in providers cannot be overridden”. amazon-bedrock is built in too and accepts only base_url, auth, http_headers and aws.* overrides, so a full gateway block under that name stops Codex with “model_providers.amazon-bedrock only supports changing base_url, auth, http_headers …”. Pick a new ID:

~/.codex/config.toml
model_provider = "corp-gateway"
[model_providers.corp-gateway]
name = "OpenAI via corporate gateway"
base_url = "https://llm-gateway.internal.example.com/v1"
env_key = "CORP_GATEWAY_API_KEY" # the variable Codex reads the key from
wire_api = "responses"
request_max_retries = 4
stream_max_retries = 10
stream_idle_timeout_ms = 300000 # raise if long turns time out mid-stream

Azure OpenAI uses the same shape with an api-version query parameter:

[model_providers.azure]
name = "Azure OpenAI"
base_url = "https://YOUR_RESOURCE.openai.azure.com/openai"
env_key = "AZURE_OPENAI_API_KEY"
query_params = { api-version = "2025-04-01-preview" }
wire_api = "responses"

Replace YOUR_RESOURCE with your Azure resource name, and use the api-version your Azure deployment documents.

Ollama and LM Studio are built-in local providers. Choose one with the top-level oss_provider key, then add --oss:

oss_provider = "ollama" # or "lmstudio"
Terminal window
codex --oss "Explain what src/billing/proration.ts does"
codex exec --oss --local-provider lmstudio "List every exported function in src/lib/"

codex exec --oss without either setting stops with “No default OSS provider configured”, and with Ollama not running it stops with “No running Ollama server detected” (both observed on 0.157.1). Both errors are safer than a silent fallback to the cloud. Before you move sensitive work to a local model, read its licence on the model card; the models hub lists which open-weight licences are verified.

How much reasoning should each Codex job get?

Section titled “How much reasoning should each Codex job get?”

Start on the default model, tune effort before you switch model, and switch model only when your own evals say so. In Codex 0.157.1 the bundled default is GPT-6 Astra at low effort; GPT-6 Sol and GPT-6 Luna default to medium. Astra accepts low through max plus the Codex-only ultra level (maximum reasoning with automatic task delegation); Luna has no ultra. Prices and context limits live on the models hub.

Put each job’s settings in its own profile. --profile NAME layers $CODEX_HOME/NAME.config.toml over your base config:

# ~/.codex/ci.config.toml — used as: codex exec --profile ci "…"
model = "gpt-6-luna" # "Fast and affordable model for easier tasks"
model_reasoning_effort = "low"
model_verbosity = "low"
model_reasoning_summary = "concise"
hide_agent_reasoning = true # keep reasoning out of CI logs
# ~/.codex/everyday.config.toml — feature work, bug fixes
model = "gpt-6-sol" # "Workhorse model for coding and everyday work"
model_reasoning_effort = "medium"
# ~/.codex/deep.config.toml — architecture reviews, gnarly debugging
model = "gpt-6-astra"
model_reasoning_effort = "high"
JobProfileWhy
Lint fixes, renames, dependency bumps in CIci (GPT-6 Luna, low; GPT-6 Sol at low if Luna’s edits fail the checks)Mechanical, high-volume edits; the test suite checks the result, and Codex’s rate-limit prompt itself recommends Luna.
Everyday feature workeveryday (GPT-6 Sol, medium)Codex labels Sol its “workhorse model for coding and everyday work”, and it stretches usage limits further than Astra.
Cross-module design, flaky-test root causesdeep (Astra, high; max or ultra only when high fails your evals)Effort costs usage; spend it where the task needs it.

If you have not measured anything yet, stay on the bundled default (Astra at low) and raise effort there first; the profiles above are where you land once your own runs show which jobs are mechanical and which need depth.

Context limits need a measured reason to change. In Codex the current models run with a 272K working window by default. You can raise it with model_context_window, up to the catalogue’s 872K ceiling, and set model_auto_compact_token_limit lower than the window so compaction happens before the window is full. For a custom provider, match both values to that provider’s published limit.

How do you set and test the sandbox boundary?

Section titled “How do you set and test the sandbox boundary?”

OpenAI now prefers permission profiles (beta, Codex CLI 0.138.0 and later) over the legacy --sandbox modes. Select one with default_permissions (or -c default_permissions='":workspace"' for one run). codex and codex exec have no dedicated flag in 0.157.1, but codex sandbox takes -P/--permission-profile:

default_permissions = ":workspace" # built-ins include :read-only, :workspace, :danger-full-access

The legacy system still works, and older configs and snippets use it:

sandbox_mode = "workspace-write"
[sandbox_workspace_write]
network_access = false # turn on only for jobs that must fetch
writable_roots = ["/Users/me/.cache/pnpm"]

Pick one system per config. OpenAI states that the profile and legacy sandbox systems “do not compose”. To give one run write access to a sibling directory without widening the whole sandbox, use --add-dir:

Terminal window
codex --cd apps/web --add-dir ../api --add-dir ../shared "Rename the Invoice DTO in all three packages"

Then test the boundary before an unattended run meets it. codex sandbox runs any command under the same sandbox Codex uses. In 0.157.1 there is no platform subcommand: everything after -- is the command.

Terminal window
# Terminal, inside the repository
codex sandbox -- sh -c 'touch /etc/probe' # expect: Read-only file system
codex sandbox -P :workspace -- sh -c 'touch ./probe && echo writable'
codex sandbox -P :workspace -- curl -sI https://registry.npmjs.org
codex sandbox -P :workspace -- npm test # does the real job fit the boundary?

On Linux, the first probe printed Read-only file system, and the network probe failed (curl exit code 6 under 0.157.1), because network access stays off until a config sets network_access = true. Run the job’s real commands (npm test, pnpm build, the migration dry-run) the same way. A command that fails here fails inside the agent run, and it costs nothing to learn that now.

How do you prove what an unattended Codex run did?

Section titled “How do you prove what an unattended Codex run did?”

Nobody reads a 40-minute CI transcript. Make the run produce evidence that a script can check.

codex exec --json prints one JSON event per line. The top-level .type is thread.started, turn.started, turn.completed, turn.failed, item.started, item.updated, item.completed or error. The kind of work is in .item.type: agent_message, reasoning, command_execution, file_change, mcp_tool_call, web_search, todo_list and others. A command_execution item carries command, exit_code and status, and a file_change item carries changes[].path. Codex reports those paths as absolute, so strip the working directory before you match them against repo-relative patterns.

Terminal window
# CI step: run, keep the evidence, then check it
set -euo pipefail # fail closed: a failed codex or git command fails the job
BASE=$(git rev-parse HEAD) # the commit the agent starts from
# legacy sandbox flag; or -c default_permissions='":workspace"' instead. Pick one system.
codex exec --profile ci --json -o summary.txt --sandbox workspace-write \
"Fix the failing unit tests in src/billing/ without changing production behaviour" \
> events.jsonl
# 1. Commands that failed or were declined
jq -r 'select(.type=="item.completed" and .item.type=="command_execution"
and ((.item.exit_code // 0) != 0 or .item.status=="declined"))
| "\(.item.exit_code)\t\(.item.command)"' events.jsonl
# 2. Files touched outside the allowed paths: fail the job if any
# Read git, not the event stream: an edit made through a shell command
# (sed -i, a heredoc) arrives as command_execution, never as file_change.
{ git diff --name-only "$BASE"; git ls-files --others --exclude-standard; } |
awk '!/^(events\.jsonl|summary\.txt|touched\.txt)$/' | sort -u > touched.txt
if grep -v -E '^(src/billing/|tests/billing/)' touched.txt; then
echo "Agent edited files outside the allowed scope"; exit 1
fi
# 3. Token usage per turn, for the cost dashboard
jq -c 'select(.type=="turn.completed") | .usage' events.jsonl

The scope check is the part that lets you stop reading every diff, and it reads git rather than the agent’s own file_change events, because a file rewritten through a shell command never produces one. The agent may write anything it likes inside src/billing/, because the tests and review gate judge that. An edit to package.json or a CI workflow is a policy violation, and the job fails before a human looks. Keep summary.txt as the PR comment, and keep events.jsonl as the build artefact. The tech lead owns the allow-list pattern and the review-gate prompt. A FAIL verdict or a scope violation goes back to the PR author, and a human reviews only the flagged findings. For a typed final answer, add --output-schema (see headless Codex).

Export OpenTelemetry to your own collector

Section titled “Export OpenTelemetry to your own collector”

For a record that outlives the CI job, export Codex’s OpenTelemetry events. Point the exporter at a collector on the same host or network:

[otel]
environment = "ci"
log_user_prompt = false # default; prompts are not exported
[otel.exporter.otlp-http]
endpoint = "http://localhost:4318/v1/logs"
protocol = "binary" # or "json"; use [otel.exporter.otlp-grpc] for gRPC

The exporter defaults to none, so nothing leaves the machine until you set it. Codex 0.157.1 emits events such as codex.conversation_starts (model, settings, sandbox policy), codex.api_request, codex.tool_decision (approved or denied, and by whom), codex.tool_result, codex.sandbox_outcome and codex.turn_cost. Alert on codex.tool_decision denials and on sandbox outcomes in unattended runs: both mean the job wanted something its boundary did not allow.

Analytics are a separate channel from OTel: they go to OpenAI, not to your collector. To switch them off, set:

[analytics]
enabled = false

notify runs a program after each turn with a JSON argument (type is agent-turn-complete, plus thread-id, turn-id, cwd, input-messages and last-assistant-message). Use it to append a line to a run log or post to chat:

notify = ["python3", "/Users/me/.codex/notify.py"]
#!/usr/bin/env python3
import json, sys, datetime
event = json.loads(sys.argv[1])
if event.get("type") == "agent-turn-complete":
with open("/Users/me/.codex/turns.log", "a") as log:
log.write(f"{datetime.datetime.now().isoformat()}\t{event.get('cwd')}\t"
f"{(event.get('last-assistant-message') or '')[:200]}\n")

Yes, but not as an MCP server any more. codex mcp-server was removed in 0.154.0, and codex mcp only manages the external servers that Codex connects to. For programmatic control, use codex exec from a shell or CI, the TypeScript SDK @openai/codex-sdk (version-locked to the CLI; 0.157.1 as of 2026-09-26), or the experimental codex app-server. The SDK runs the same agent, so AGENTS.md, skills and config all apply, and it persists threads in ~/.codex/sessions so resumeThread(id) can pick one up again. Patterns and trade-offs live in the Codex SDK guide. If you are choosing between SDKs across tools, see driving agents from code.

How do you keep a shared Codex config honest?

Section titled “How do you keep a shared Codex config honest?”
  • codex features list prints every feature with its stage and effective state. In 0.157.1, shell_snapshot, unified_exec, goals, hooks, multi_agent and worktrees are stable and on; memories is stable but off.
  • --enable NAME or --disable NAME changes a feature for one run. codex features enable NAME writes it into config.toml.
  • Flags whose stage is removed (for example tool_search, steer, js_repl) do nothing when toggled. Delete them from shared config.
  • codex doctor --json emits a redacted report of install, config, auth and runtime health. Attach it to bug reports instead of pasting your config.
  • Codex refuses to start after you add a provider. The error names a reserved built-in ID (openai, ollama, lmstudio) or, for amazon-bedrock, lists the only fields you may override. Rename the block, for example [model_providers.corp-gateway], and set model_provider to the new name.
  • A snippet from an old article fails. Under --strict-config, remove the key the error names. Old commands fail too: --full-auto now gives “unexpected argument” (use a permission profile, --approve-for-me or --sandbox workspace-write), and codex mcp-server no longer exists. Legacy [profiles.NAME] tables still parse, but move them to $CODEX_HOME/NAME.config.toml so --profile layers them the current way.
  • The gateway returns 401. The variable named in env_key is unset in the shell that launched Codex. CI runners do not inherit your login shell. Export it in the job step and check with codex doctor --json.
  • Long turns die with stream timeouts. Raise stream_idle_timeout_ms and stream_max_retries on your custom provider. For the built-in provider, first check whether a proxy in between is closing idle connections.
  • No OTel events arrive. Check that the exporter table is [otel.exporter.otlp-http] or [otel.exporter.otlp-grpc] (the default is none), and that the collector path ends in /v1/logs for HTTP. Then check the header caution above.
  • The agent run fails on a command that works in your shell. Re-run it with codex sandbox -- COMMAND. If it fails there too, add a writable_roots entry or --add-dir for that path, or turn on network for that profile only, instead of switching to danger-full-access.
  • The scope check fails on generated files. Lockfiles and snapshots count as edits. Add their exact paths to the allow pattern, or regenerate them in a separate, non-agent CI step.

Where to go next with Codex infrastructure

Section titled “Where to go next with Codex infrastructure”