What it is
Codex runs commands with your permissions. Whether a hijacked session can read your keys, reach the network or run a repository's code depends on your config.toml, your command rules and what the project ships in .codex/. The audit checks all of it against the Security for Codex Development guide and tells you exactly what to change.
OpenAI Codex security audit v1.0.0 (read-only, offline)
FAIL [4] .codex/config.toml lowers your protections
approval_policy = never
network_access = true
FAIL [4] .env sets Codex variables
CODEX_HOME
WARN [2] Login shells are allowed (the default)
Login shells load ~/.bashrc and ~/.zprofile, including any secrets exported there.
PASS [2] A permission profile is the default (dev)
PASS [2] The permission profile denies .env files, ~/.ssh and ~/.aws
PASS [3] Command rules ask before git push and guard rm
...
Summary: 12 pass, 1 warn, 2 fail, 1 info
Save a report with how-to-fix steps? [h]tml, [t]ext, [c]sv, [j]son, [n]o (default n):Every finding shows, in brackets, the section of the Codex guide that explains the fix.
Why you can trust it
| Small and known | Official Alpine Linux 3.24.2 pinned by digest, plus only bash, jq and yq (to read Codex's TOML config) from Alpine's repositories, pinned to exact versions. |
| Open | Everything that runs is the Dockerfile, audit.sh and its mappings file. You build it yourself from source. |
| Offline | Runs with --network none: it cannot send anything anywhere. |
| Read-only | Your files are mounted read-only and the container's own filesystem is read-only. It changes nothing. |
| What it can read | ~/.codex/config.toml, hooks.json, rules/ and AGENTS.md, your shell startup files, and the whole project folder you run it from, including any secrets kept in that project. It doesn't mount auth.json, transcripts, history, logs or the rest of your home folder. Its output names settings, files, commands and variable names; it never prints secret values or the contents of .env files. |
| Unprivileged | Runs as you, with every Linux capability dropped, and is removed when it exits. |
Run it
You need Docker Desktop, Docker Engine or Podman. Nothing else is installed on your machine.
macOS, Linux and WSL2
git clone https://github.com/aisecuritylabs-org/coding-agent-security.git ~/coding-agent-security
cd ~/code/my-app # the project you want to audit
bash ~/coding-agent-security/codex/audit/run.shWindows: Command Prompt
git clone https://github.com/aisecuritylabs-org/coding-agent-security.git "%USERPROFILE%\coding-agent-security"
cd /d C:\Projects\my-app
powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\coding-agent-security\codex\audit\run.ps1"Windows: PowerShell
git clone https://github.com/aisecuritylabs-org/coding-agent-security.git "$HOME\coding-agent-security"
Set-Location C:\Projects\my-app
powershell -NoProfile -ExecutionPolicy Bypass -File "$HOME\coding-agent-security\codex\audit\run.ps1"Replace the project folder with the one you want to audit. On Windows the launcher also checks who can write C:\ProgramData\OpenAI\Codex, which the container can't see.
What it checks
| Area | Examples |
|---|---|
| Sandbox and approvals | No full access; a permission profile as the default; on-request approvals, not the retired untrusted or never; auto-review flagged |
| Secrets and network | .env, ~/.ssh and ~/.aws denied; command network off or behind the proxy; no exfiltration-prone domains; web search not live (and disabled on Windows); keyring credential stores; no login shells; no secrets in MCP env blocks |
| Rules and hooks | Rules ask before git push and guard rm; no allow rules for shells, deletes or network tools; no hook or notify command that runs remote code |
| Extensions | MCP server inventory; unpinned servers; servers that read outsider-written content; skills that install MCP servers; memories from web and MCP content |
| Repositories | .codex/config.toml that lowers protections or starts MCP servers; project hooks and allow rules; a .env that sets CODEX_* variables; AGENTS.md fetch instructions; disguised symlinks; .git/config keys that run programs |
| Windows and hygiene | ProgramData folder permissions; machine-wide settings; --yolo and bypass aliases; Codex version |
The exit code is 0 with no FAIL results and 1 otherwise, so the audit also works in scripts and CI.
Real attacks it covers
| Attack | What it did | Check |
|---|---|---|
| CVE-2025-61260 (Check Point) | A project .env pointed CODEX_HOME into the repository, so its MCP servers ran on open | P04, P03 |
| CVE-2026-19590, -19592, -19593 | A repository's own .git/config made git run an attacker program | P12 |
| ProgramData configuration (Cymulate) | One Windows user plants settings every Codex user loads | W01, W02 |
| Windows web search chain (Cymulate) | Web content plus a PATH hijack ran code outside the sandbox | C05 |
| Auto-review approves malware (PromptArmor) | A hidden instruction in an issue got a malicious install approved | C09 |
| Agentjacking | Fake error reports told the agent to run the attacker's command | M02, P08 |
Reports with how-to-fix steps
After the on-screen summary, the audit asks whether to save a report as HTML,text, CSV or JSON. Every finding explains why it matters, gives the exact fix, maps the risk to OWASP Top 10 for LLM Applications 2025, OWASP Top 10 for Agentic Applications 2026, MITRE ATLAS 2026.09 and NIST AI RMF 1.0, and lists its sources. Reports are saved to codex-audit-reports in your home folder, never inside the project. Reports mask values that look like secrets, such as tokens, passwords and credentials in URLs, but review a report before you share it.
Fix what it finds
Most findings are fixed by two files: a ~/.codex/config.toml built on a permission profile, and command rules. Both are below as examples to adapt and test, and every key is from OpenAI's configuration reference.
Baseline config.toml
Adapt the deny paths and, when a task needs network, uncomment the registry allow list. Don't also set the older sandbox_mode keys: Codex uses those instead of the profile if any loaded config sets them.
# ~/.codex/config.toml: AISecurityLabs.org baseline for a developer laptop.
#
# Every key is from the Codex configuration reference
# (https://developers.openai.com/codex/config-reference). Check them against
# the reference for your Codex version before relying on them.
#
# This file uses a permission profile, which is the only way to deny reads of
# secrets from your own config. Don't also set the older sandbox_mode or
# [sandbox_workspace_write] keys: Codex uses those instead of the profile if
# any loaded config sets them.
# Ask before anything leaves the sandbox. "untrusted" is retired and "never"
# removes every approval.
approval_policy = "on-request"
approvals_reviewer = "user"
default_permissions = "dev"
# Cached results come from an OpenAI-maintained index. Use "disabled" on native
# Windows and for regulated repositories; never "live" for routine work.
web_search = "cached"
# Keep Codex and MCP credentials in the OS keyring, not in a file.
cli_auth_credentials_store = "keyring"
mcp_oauth_credentials_store = "keyring"
# Commands don't start login shells, so ~/.bashrc, ~/.zprofile and the secrets
# they often export aren't loaded.
allow_login_shell = false
[features]
# Enforces the domain allow list below whenever a profile turns network on.
network_proxy = true
# Skills don't install and start MCP servers you haven't reviewed.
skill_mcp_dependency_install = false
[memories]
# Content from web search or MCP tools can't become a lasting memory.
disable_on_external_context = true
[permissions.dev]
description = "Edit the workspace; keep secrets unreadable; no network by default."
extends = ":workspace"
[permissions.dev.filesystem]
"~/.ssh" = "deny"
"~/.aws" = "deny"
"~/.config/gcloud" = "deny"
"~/.azure" = "deny"
"~/.kube" = "deny"
"~/.docker" = "deny"
"~/.npmrc" = "deny"
"~/.netrc" = "deny"
# Bounds the glob scan for the workspace rules below on Linux, WSL and Windows.
glob_scan_max_depth = 4
[permissions.dev.filesystem.":workspace_roots"]
"**/.env*" = "deny"
"**/*.pem" = "deny"
"**/*.key" = "deny"
# Command network access stays off. When a task needs a package registry,
# uncomment these lines; the proxy then allows only the listed hosts.
# [permissions.dev.network]
# enabled = true
#
# [permissions.dev.network.domains]
# "registry.npmjs.org" = "allow"
# "pypi.org" = "allow"
# "files.pythonhosted.org" = "allow"web_search and the profile's network setting are separate switches, and the network proxy doesn't filter web search, MCP servers or connectors, so set each one (Codex docs).
Command rules
Save this as ~/.codex/rules/default.rules and restart Codex. Codex validates thematch examples when it loads the file, and you can test any command yourself:
codex execpolicy check --pretty --rules ~/.codex/rules/default.rules -- git push origin main# ~/.codex/rules/default.rules: AISecurityLabs.org command rules for Codex.
#
# Rules match the start of a command: "prompt" asks you before it runs,
# "forbidden" refuses it, and "allow" lets it run outside the sandbox without
# asking. When several rules match, the strictest wins. A rule with no decision
# is an allow rule, so every rule here states its decision.
#
# Codex splits simple `bash -lc "a && b"` scripts into separate commands before
# checking rules. Scripts it can't split (redirections, $(...), variables) are
# checked as one `bash` command, which the last rule sends to you for approval.
# Remove that rule if the prompts get in your way.
#
# Codex checks the match / not_match examples when it loads this file.
# Test a command with:
# codex execpolicy check --pretty --rules ~/.codex/rules/default.rules -- git push
# Restart Codex after editing.
prefix_rule(
pattern = ["git", "push"],
decision = "prompt",
justification = "Pushing publishes code: read what is being pushed first",
match = ["git push", "git push origin main"],
not_match = ["git status"],
)
prefix_rule(
pattern = ["git", ["reset", "clean"]],
decision = "prompt",
justification = "Discards local work",
match = ["git reset --hard", "git clean -fdx"],
)
prefix_rule(
pattern = ["rm", ["-rf", "-fr", "-r", "-R"]],
decision = "forbidden",
justification = "Recursive deletes are blocked: delete the files yourself if you really mean it",
match = ["rm -rf build", "rm -r dist"],
not_match = ["rm notes.txt"],
)
prefix_rule(
pattern = [["sudo", "su", "doas"]],
decision = "forbidden",
justification = "No privilege escalation from the agent",
match = ["sudo apt install jq"],
)
prefix_rule(
pattern = [["curl", "wget"]],
decision = "prompt",
justification = "Network transfers can download code or upload data",
match = ["curl https://example.com", "wget https://example.com/x"],
)
prefix_rule(
pattern = [["npm", "pnpm", "yarn"], "publish"],
decision = "prompt",
justification = "Publishing a package",
match = ["npm publish", "pnpm publish --access public"],
)
prefix_rule(
pattern = [["terraform", "kubectl", "helm"]],
decision = "prompt",
justification = "Infrastructure and cluster commands",
match = ["terraform apply", "kubectl delete pod web"],
)
prefix_rule(
pattern = [["bash", "sh", "zsh", "pwsh", "powershell"]],
decision = "prompt",
justification = "A shell entrypoint can hide any command",
match = ["bash -c 'echo hi; echo there > out.txt'"],
)Rules match the start of a command, so they're guardrails rather than walls; keep the permission profile underneath. To prove it all works in a live session, run theten-minute self-test.
Windows: lock the ProgramData folder
Codex on Windows loads machine-wide settings from C:\ProgramData\OpenAI\Codex. The folder isn't created or locked by default, so any standard user could create it and plant settings, such as a notify command or a weaker sandbox, that every Codex user on the machine loads. Cymulate reported this and OpenAI had not committed to a fix at publication (Cymulate). The audit reports it as W01.
Create the folder yourself before anyone else can, owned by Administrators, with write access only for Administrators and SYSTEM and read access for everyone else, and apply the same to anything already inside it. Run this in PowerShell opened with Run as administrator; it's also in the repository as codex/config/lock-programdata.ps1, and the last line shows the result:
# Run in PowerShell opened with "Run as administrator".
$p = 'C:\ProgramData\OpenAI\Codex'
New-Item -ItemType Directory -Force -Path $p | Out-Null
# A fresh ACL: owned by Administrators, no inherited or extra entries.
$admins = [Security.Principal.SecurityIdentifier]'S-1-5-32-544'
$acl = New-Object Security.AccessControl.DirectorySecurity
$acl.SetOwner($admins)
$acl.SetAccessRuleProtection($true, $false)
$grants = @(
@('S-1-5-32-544', 'FullControl'), # Administrators
@('S-1-5-18', 'FullControl'), # SYSTEM
@('S-1-5-32-545', 'ReadAndExecute') # Users: read only
)
foreach ($g in $grants) {
$sid = [Security.Principal.SecurityIdentifier]$g[0]
$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($sid, $g[1], 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
}
Set-Acl -LiteralPath $p -AclObject $acl
# Anything already inside takes the same owner and permissions.
Get-ChildItem -LiteralPath $p -Recurse -Force | ForEach-Object {
icacls $_.FullName /setowner '*S-1-5-32-544' /c /q | Out-Null
icacls $_.FullName /reset /c /q | Out-Null
}
icacls $pIf the folder already existed and you didn't create it, check what's inside first, and remove anyconfig.toml or requirements.toml your administrators didn't put there. Then re-run the audit: W01 passes only when the folder and everything in it are owned and writable by Administrators, SYSTEM or TrustedInstaller alone.
Delete it when you are done
The container is already gone when the audit finishes. To remove the image as well:
docker image rm codex-audit