GUIDE · HARDENING 11 min ·

Codex CLI hardening guide 2026Config, approval modes, and the foot-guns.

Codex CLI ships with full-auto mode and ambient API key handling. Neither is safe for production work. Three config changes cut most of the exposure.

TL;DR· the answer, in twenty seconds

What: Codex CLI defaults to full-auto approval mode and inherits your full shell environment, including OPENAI_API_KEY. The key appears in session transcripts on disk and is not redacted from log output by default.

Fix: Set approval-mode = "suggest" and sandbox = "workspace-read" in ~/.codex/config.toml. Deliver the API key through a process-scoped injector rather than exporting it in your shell.

Lesson: Any agent that inherits your full environment and can run commands without approval is one misread instruction away from a production incident. The config file is small. Read it before the first real session.

OpenAI's Codex CLI reached general availability in early 2026 and immediately attracted the same configuration mistakes that hit Claude Code before it. The defaults are designed for demos. A developer who reads the README, installs the tool, and starts working in a production repo will likely be running with the same config a week later.

The config file is ~/.codex/config.toml. It is small. Most of the interesting decisions live in four settings. This guide covers each one, shows a hardened baseline, and names the five most common mistakes.

If you shipped a Codex CLI session with OPENAI_API_KEY in your shell environment and logged the session, read the transcript section before anything else.

What to know in 60 seconds

  • approval-mode controls whether the agent asks before running commands. The default, full-auto, does not ask.
  • sandbox controls filesystem scope. workspace-write (the default) lets the agent modify any file in the project tree. workspace-read limits writes to explicitly approved paths.
  • OPENAI_API_KEY is not redacted from session transcripts by default. If you log or export those transcripts, the key goes with them.
  • MCP servers Codex CLI connects to are not verified by signature. Anything in the mcp-servers block in your config runs with the agent's permissions.
  • Session transcripts land in ~/.codex/sessions/ as JSON files. They include the full conversation, tool calls, and command outputs, in plaintext.

Understand the config file structure

Codex CLI reads ~/.codex/config.toml on startup. A fresh install produces something close to this:

model = "codex-1"
approval-mode = "full-auto"
sandbox = "workspace-write"

[mcp-servers]
# none by default

There is no project-level override file yet (as of March 2026). All settings are global unless you wrap the binary in a shell script that rewrites the config before each invocation. That gap matters when you work across repos with different risk profiles.

The full list of recognized keys:

Key Values Default
model any OpenAI model string codex-1
approval-mode suggest, auto-edit, full-auto full-auto
sandbox workspace-read, workspace-write workspace-write
notify true, false true
mcp-servers table of server definitions empty
deny array of shell glob patterns empty
allow array of shell glob patterns empty

Pick the right approval mode

approval-mode is the highest-leverage setting. The three options differ in what the agent does without asking:

suggest shows every proposed command before execution. The agent cannot run anything until you press Enter. This is the only mode where you retain meaningful oversight of destructive operations.

auto-edit runs file edits silently but pauses for shell commands. If the agent's mental model of your project is wrong, it will silently overwrite files until it hits a shell command. That can be a lot of silent damage before the first approval prompt.

full-auto runs everything without asking. It is appropriate for sandboxed CI jobs where the entire workspace is disposable. It is not appropriate for any session that touches files you care about or credentials that are live.

The Replit incident in mid-2024, where an agent deleted a production database, happened because a full-auto equivalent mode ran a destructive command the developer did not anticipate. The command was correct given the agent's (wrong) understanding of the task. The developer had no chance to intercept it.

Set approval-mode = "suggest" for all interactive work. Use full-auto only in CI with an ephemeral filesystem and no live credentials in scope.

approval-mode = "suggest"

Lock down the sandbox

workspace-write lets the agent create, modify, and delete any file inside the project directory. workspace-read treats the workspace as read-only and requires explicit approval for each write path.

With workspace-read, the agent can still read your entire project tree. It can execute shell commands (subject to approval-mode). It cannot silently modify files you did not approve. The difference matters most when the agent is trying to fix a bug in one file and decides to refactor three others on the way.

sandbox = "workspace-read"

If you need the agent to make edits, approve each path at the prompt. That approval is logged. A workspace-write session with full-auto is not.

Allowed and denied commands

allow and deny take shell glob patterns. They filter the commands the agent can run, before the approval prompt fires. A deny match blocks the command entirely, regardless of mode.

deny = [
  "rm -rf *",
  "git push --force*",
  "curl * | sh",
  "wget * | sh",
  "* | bash",
  "* | zsh",
  "chmod 777*",
  "sudo *",
  "gh release delete*",
  "heroku pg:reset*",
]

deny patterns match against the full command string the agent constructs. They do not parse shell syntax, so rm -rf / and rm -rf with a path argument are different strings. Test your patterns against real command strings from your session transcripts before relying on them.

Fix the OPENAI_API_KEY logging gap

Codex CLI does not redact OPENAI_API_KEY from session transcripts or stdout by default. If the agent calls a tool or runs a command that echoes environment variables, the key appears in the output verbatim.

Session transcripts live at ~/.codex/sessions/<session-id>.json. Each file contains the full conversation history, including all tool call inputs and outputs. An agent invocation that prints env or printenv writes the key to the transcript. So does any command that logs its own configuration if that configuration includes the API key.

This is not theoretical. Any session where the agent debugs an environment issue, checks its own context, or runs a tool that dumps configuration state is a candidate for key capture.

Two places you need to check if you have run Codex CLI with the key in your shell:

# Check transcript directory for key material
grep -r "sk-" ~/.codex/sessions/
grep -r "OPENAI_API_KEY" ~/.codex/sessions/

If you find matches, rotate the key at platform.openai.com before anything else.

To stop the key from reaching transcripts in future sessions, do not export it as a shell variable. Instead, inject it at the process level for each Codex CLI invocation:

OPENAI_API_KEY="$(cat ~/.secrets/openai-key)" codex

Or use a tool that scopes the key to the child process and removes it when the session ends. The point is that a key sitting in $OPENAI_API_KEY in your shell is available to every process that inherits your environment, including anything the agent spawns. A process-scoped delivery means the key exists for exactly one invocation.

This is the exact pattern hasp is built around. Bind the key in the hasp vault, run hasp run -- codex, and the broker injects OPENAI_API_KEY into that one child process. The key never sits in $OPENAI_API_KEY in your shell profile. No subprocesses inherit it. No session transcript captures it from a command that prints the environment.

Allowlist MCP servers explicitly

The [mcp-servers] table in config.toml controls which Model Context Protocol servers Codex CLI connects to. There is no signature requirement on MCP server packages. OX Security documented roughly 7,000 MCP servers in the wild as of early 2026, with around 150 million cumulative downloads and no attestation layer. A server you install from npm or a GitHub repo runs with the agent's full permissions.

If you are not using MCP servers, leave the table empty and keep it empty:

[mcp-servers]
# intentionally empty

If you need a specific server, define it explicitly by path. Do not use * wildcards in the server list. Treat each MCP server like a dependency: review the source, pin the version, and know what it can do.

[mcp-servers]
my-local-tool = { command = "/usr/local/bin/my-tool", args = [] }

Do not install MCP server packages from registries you have not reviewed. A server that calls home on first connection and receives a payload is not a theoretical attack. The MCP GitHub prompt-injection data exfiltration that Snyk researchers disclosed in early 2026 ran through a trusted-looking server that was not trusted.

Where session transcripts live on disk

Transcripts accumulate in ~/.codex/sessions/. Each file is named with a UUID and timestamped. They are JSON and readable as plaintext. The directory is not bounded in size by default.

# List sessions by date, newest first
ls -lt ~/.codex/sessions/

# Show the most recent transcript
ls -t ~/.codex/sessions/ | head -1 | xargs -I{} cat ~/.codex/sessions/{}

Transcripts include: the full message history, every tool call name and argument set, every command the agent ran, and all command output including stderr. Any credential that passed through a command output, a tool response, or a debug message is in the transcript.

Clean up sessions after each project:

# Remove sessions older than 7 days
find ~/.codex/sessions/ -name "*.json" -mtime +7 -delete

If you pipe transcripts to a logging system, bug tracker, or team dashboard, audit what is in them before the next send.

The five most common foot-guns

These mistakes show up repeatedly in incident reports and developer forum threads from January through March 2026.

1. Running full-auto in a repo with live credentials in scope. The agent cannot distinguish "this command is fine" from "this command will drop your database." It executes what it calculates as the next correct step. You get no interception point.

2. Exporting OPENAI_API_KEY in .zshrc or .bashrc. Every Codex CLI session, every child process the agent spawns, and every other tool that runs in that shell inherits the key. A transcript capture, a debug log, or an MCP server that reads environment variables pulls it out.

3. Installing MCP servers from community lists without reading the source. MCP servers run with the agent's permissions. A server that reads your filesystem and POSTs the contents to an external URL has everything it needs to exfiltrate credentials.

4. Leaving session transcripts in ~/.codex/sessions/ indefinitely. The directory grows without a cap. A transcript from a session that ran env in 2026 is still there in 2027 with your rotated key's predecessor sitting in plaintext.

5. Treating deny patterns as a security boundary. The deny list filters string matches against command strings the agent constructs. It does not sandbox the process or intercept syscalls. An agent that constructs a multiline heredoc, pipes through eval, or chains commands through a wrapper can bypass a glob-match filter. Use deny as a speedbump, not a wall.

What the conventional hardening advice misses

Most guides for Codex CLI hardening focus on approval-mode. Set it to suggest, they say, and you are covered. That is not wrong, but it misses two things.

First, approval-mode = "suggest" does not protect against a misconfigured MCP server that exfiltrates data through a tool call. The agent asks your approval before running shell commands. It does not ask before calling a tool that the server registered. If the server is malicious or compromised, the damage happens at the tool-call layer, which approval-mode does not cover.

Second, the API key logging gap does not go away with suggest mode. The key is still in your environment. The agent still has access to it. If any approved command, any tool response, or any debug output echoes it, it lands in the transcript. Approval mode controls command execution, not environment inheritance.

The gap between "I approved that command" and "I know what information the agent processed and logged" is where most incidents actually land.

A hardened baseline

Paste this into ~/.codex/config.toml and adjust model and deny patterns for your workflow:

model = "codex-1"
approval-mode = "suggest"
sandbox = "workspace-read"
notify = true

deny = [
  "rm -rf *",
  "git push --force*",
  "git push --delete*",
  "curl * | sh",
  "curl * | bash",
  "wget * | sh",
  "wget * | bash",
  "* | bash",
  "* | zsh",
  "* | sh",
  "chmod 777*",
  "sudo *",
  "su *",
  "gh release delete*",
  "gh repo delete*",
  "heroku pg:reset*",
  "dropdb *",
  "redis-cli flushall*",
]

[mcp-servers]
# add only explicitly reviewed and pinned servers

Do not export OPENAI_API_KEY in your shell profile. Deliver it per invocation.

A checklist you can paste into a PR

## Codex CLI hardening checklist

- [ ] ~/.codex/config.toml exists and is version-controlled separately from any repo
- [ ] approval-mode = "suggest" (not full-auto or auto-edit)
- [ ] sandbox = "workspace-read"
- [ ] deny block covers rm -rf, force push, pipe-to-shell, sudo
- [ ] OPENAI_API_KEY not exported in ~/.zshrc, ~/.bashrc, or ~/.profile
- [ ] OPENAI_API_KEY delivered per-invocation or via process-scoped injector
- [ ] grep -r "sk-" ~/.codex/sessions/ returns nothing (or transcripts already rotated)
- [ ] ~/.codex/sessions/ cleanup scheduled (cron or manual, at least weekly)
- [ ] [mcp-servers] table is empty or contains only reviewed, pinned servers
- [ ] No MCP server package installed from a registry without source review
- [ ] CI jobs that use full-auto run with ephemeral filesystems and no live credentials
- [ ] Session transcripts are not piped to external logging without credential scrubbing

Run this before your first production session and after any change to config.toml.

What this means for your stack

The codex CLI config file is thirty lines. The attack surface it governs is not. full-auto mode, ambient API key exposure, and MCP servers with no attestation layer are three independent paths to an incident. Any one of them is enough.

The durable fix is the same one that applies to every coding agent: do not hand the agent ambient long-lived credentials. Deliver a scoped value into one child process at exec time, then take it away. An agent that never holds the real key cannot log it, cannot write it to a transcript, and cannot hand it to a misbehaving MCP server.

hasp is one working implementation of that model. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, bind a project, and the next Codex CLI session receives a process-scoped reference instead of your live key. The broker injects the value at exec time, the session ends, and the key is gone from the process tree. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

The hardening checklist above works without any additional tooling. But the config file cannot fix the runtime model. As long as the key lives in your environment, it travels with the agent everywhere the agent goes.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index