Skip to content
All posts
8 min readBrian McManus

Codex requirements.toml for security teams: approvals, sandbox modes and MCP allowlists

The admin-enforced layer in OpenAI Codex, key by key: where requirements.toml is read from, how sandbox and approval allowlists work, how MCP server identities are matched, and what the network proxy does not filter.

codexenterprisemcppolicy

requirements.toml is where an administrator sets limits a Codex user cannot override: approval policies, sandbox modes and permission profiles, which MCP servers the client may enable, which paths it may read, and which domains commands may reach. It is read from /etc/codex/requirements.toml on Linux and macOS and %ProgramData%\OpenAI\Codex\requirements.toml on Windows, with three more delivery layers above it. The details below come from OpenAI's managed configuration documentation for Codex CLI 0.166.0.

Which layer wins

OpenAI lists four sources, applied from lowest to highest priority:

  1. System requirements.toml, at the paths above.
  2. Agent Security requirements in the cloud configuration bundle, delivered at sign-in on a supported plan.
  3. Legacy managed_config.toml fields the client reinterprets as requirements.
  4. macOS managed preferences, through com.openai.codex:requirements_toml_base64.

Higher layers override ordinary scalar and list values, and tables merge by key, though OpenAI notes that rules, hooks and filesystem restrictions have field-specific composition. What happens on a conflict is the useful part: when config.toml, a profile file or a CLI override conflicts with an enforced rule, the client falls back to a compatible value and tells the user. A requirement is a ceiling, not a suggestion.

Approvals and sandbox modes

The two keys most deployments start with:

allowed_approval_policies = ["on-request"]
allowed_sandbox_modes = ["read-only", "workspace-write"]

read-only restricts writes, workspace-write permits them inside the workspace and configured writable roots, and danger-full-access removes the filesystem sandbox, which is the mode most people are trying to prevent by writing this file. Note that approval_policy = "untrusted" is retired: keep untrusted in the allowlist if you rely on the trust_level = "untrusted" behavior it derives from, but remove it as an explicit setting.

allowed_approvals_reviewers = ["auto_review"] requires automatic review, while including "user" allows manual approval.

Permission profiles replace sandbox modes

Codex 0.138.0 introduced permission profiles, and OpenAI now recommends them over allowed_sandbox_modes. The table lists every profile a user may select, and omitted profiles are denied, including built-ins added later:

default_permissions = "acme_workspace"

[allowed_permission_profiles]
":read-only" = true
":workspace" = true
acme_workspace = true
# ":danger-full-access" is omitted, so it is denied.

[permissions.acme_workspace]
description = "Workspace access with sensitive files denied."
extends = ":workspace"

[permissions.acme_workspace.filesystem.":workspace_roots"]
"**/*.env" = "deny"

Clients on 0.137.0 and earlier ignore the profile table, so check versions first.

Deny reads, independent of the sandbox default

A read-only default does not hide files. Filesystem requirements do:

[permissions.filesystem]
deny_read = [
  "/**/*.env",
  "~/.ssh",
  "/**/*.pem",
]

Absolute paths and ~-relative paths are accepted; relative paths beginning with ./ are not. When deny-read requirements are present, the runtime rejects full-access permissions and keeps execution in a read-only or workspace sandbox so the rule can be enforced at all. On native Windows, managed deny_read covers direct file tools; shell subprocess reads do not use that sandbox rule.

Command rules

[rules] merges with any .rules files on the machine, and the most restrictive decision wins. A requirement must name decision, and that decision must be prompt or forbidden:

[rules]
prefix_rules = [
  { pattern = [{ token = "rm" }], decision = "forbidden", justification = "Use git clean -fd instead." },
  { pattern = [{ token = "git" }, { any_of = ["push", "commit"] }], decision = "prompt", justification = "Require review before mutating history." },
]

The MCP allowlist

The client enables a server only when both its name and its identity match an approved entry:

[mcp_servers.docs]
identity = { command = "codex-mcp" }

[mcp_servers.remote]
identity = { url = "https://mcp.acme.com/mcp" }

The string form of identity.command matches only the configured command. It does not inspect args, cwd, env or env_vars, so it approves a binary rather than an invocation. To constrain the whole call, write the structured form:

[mcp_servers.internal.identity]
command = { executable = "/usr/local/bin/codex-mcp", args = [
  { match = "exact", value = "serve" },
  { match = "prefix", value = "--workspace=" },
] }

The executable, the argument count and the argument order all have to match, and values support exact, prefix and full-value regex. Working directory and environment are still unchecked, so a policy that needs those has to constrain them elsewhere.

Two edge cases. An mcp_servers table that is present but empty disables every MCP server, which is a clean way to say "no MCP yet" during a rollout. And plugin-bundled servers use the same identity shapes under plugins.<plugin>.mcp_servers.<server>, so a plugin can reintroduce a server you excluded at the top level.

The network proxy, and what it does not filter

In 0.166.0 [network_proxy] is canonical and [experimental_network] is the older name still accepted. Use one name per layer:

[network_proxy]
enabled = true
managed_allowed_domains_only = true

[network_proxy.domains]
"api.openai.com" = "allow"
"**.acme.com" = "allow"
"*.exfil.example.com" = "deny"

enabled = true starts the proxy independently of the user setting, though the active sandbox must also allow command network access. managed_allowed_domains_only makes administrator entries exclusive and is only meaningful when you define allow entries: with no managed allow rule, commands have no effective destinations. *.example.com matches subdomains, **.example.com matches the apex and its subdomains, and a matching deny wins.

OpenAI states the boundary plainly. The proxy routes commands inside the sandbox, and does not filter web search, apps and connectors, MCP server connections, native-app traffic, or the client's own service requests. Browser tools check managed denies before reaching an origin, which is a separate policy check rather than routing browser traffic through the proxy. Each surface has its own key, from allowed_web_search_modes and features.plugins to the browser and computer-use requirements.

One caveat: where policy permits full sandbox escalation and a user obtains it, that execution can bypass the command proxy, so set the approval and sandbox requirements for the boundary you intend.

Managed hooks and telemetry

Managed hooks live under [hooks], with managed_dir pointing at the directory where your device management installs the scripts, and allow_managed_hooks_only = true skips hooks from user, project, session and plugin sources. The runtime enforces the configuration but does not distribute the scripts.

OpenAI documents the failure behavior: a supported denial blocks an action, but a PreToolUse callback error, timeout or malformed response can fail without blocking the tool, and MCP hooks do not provide a complete Compliance API audit trail.

[otel] sets the log, trace and metrics exporters and controls which content Codex exports. Support is version-specific: the macOS desktop app build that bundles CLI 0.166.0-alpha.5.1 does not support [otel] in requirements.toml, so verify the build before claiming the setting is in force.

What requirements.toml leaves uncovered

It binds Codex. Cursor, Claude Code and GitHub Copilot on the same laptop have their own settings and MCP configurations, and their servers are outside this table.

It needs a delivery path you control. A system file is editable by anyone with administrator rights on an unmanaged device, and the cloud bundle only reaches signed-in clients on a supported plan. Codex also ignores allowed_login_methods, allowed_chatgpt_workspaces, cli_auth_credentials_store and chatgpt_base_url in cloud-managed requirements, so those four need a local file or an MDM profile.

Host patterns are policy selection, not device proof. [[remote_sandbox_config]] compares hostname_patterns against the best-effort resolved host name and overrides only allowed_sandbox_modes; OpenAI says not to read the match as authenticated device identity.

There is no inventory. Requirements say what is enforced on a machine that received them, not which endpoints have the file, which versions are down-level, or which MCP servers are configured around it.

Where Alyria Beacon adds a layer

Alyria Beacon is a signed, user-space agent for macOS, Linux and Windows that runs beside Codex rather than wrapping it: no kernel driver, read-only install, monitor mode first. It inventories every AI tool and MCP server configured on the endpoint, including the stdio servers Codex's own configuration does not describe and the clients that never received a requirements.toml. Lyra policy is evaluated locally on every model and tool call, so one rule set covers Codex, Cursor, Claude Code and GitHub Copilot, and sequence policies cover what a per-command rule cannot: after a secret is read, block egress for the rest of the session. Decisions land in one signed, hash-chained audit chain that Spectra routes to your SIEM, and Umbra brokers short-lived credential leases so an agent never holds the standing key.

Keep the requirements file. It is the strongest control Codex offers, it costs nothing to run, and the endpoint layer exists to cover the parts of the fleet the file cannot reach.

A rollout order

  1. Ship allowed_approval_policies and permission profiles, with :danger-full-access absent from the table.
  2. Add [permissions.filesystem] deny_read for .env, keys and cloud credential directories.
  3. Add the MCP identities you approve, in structured form, including plugin paths.
  4. Add [rules] for the commands you cannot tolerate, then the network proxy with an explicit allow list.
  5. Inventory the fleet read-only, including which machines never received the file and which other agents need the same rules.

Frequently asked questions

Does requirements.toml stop a user running Codex with no sandbox? If allowed_sandbox_modes or allowed_permission_profiles omits the permissive option, yes. The client falls back to a compatible value and notifies the user, so the flag stops working.

Is the MCP allowlist the same as an approval prompt? No. It decides whether the client enables a server. Approval policy decides when the agent must ask before a command runs, and a deployment needs both.

/integrations/codex shows the Codex controls beside the endpoint layer, and Blocking a coding agent's tool call before it runs covers the per-call enforcement question a managed configuration leaves open.

How is your team using AI agents?

Pick your function and answer 20 questions on agent workflows and controls. Your result appears on the page.

Governed AI agents for the work your teams do.