# Codex Enterprise Rollout — Push ACP to Every Seat with requirements.toml

A complete requirements.toml that puts ACP's policy hook on every Codex seat from a system file, a macOS MDM profile, or the ChatGPT Enterprise workspace's managed configuration, with the MCP allowlist entry, the MDM script drop, per-person enrollment, and the legacy-field trap.

# Roll ACP out to every Codex seat

Codex has the most explicit enterprise channel of any harness: `requirements.toml`, a file of admin-enforced constraints that users can't override, delivered by a system path, an MDM profile, or the ChatGPT Enterprise workspace itself. OpenAI's [admin rollout guide](https://learn.chatgpt.com/docs/enterprise/admin-setup) and [managed configuration reference](https://learn.chatgpt.com/docs/enterprise/managed-configuration) are the source for every key below. Read the [shape all four harnesses share](/docs/enterprise) first if you haven't.

## Before you push

1. Claim your company domain on the workspace's Team page so enrollments land in your workspace ([why](/docs/enterprise#who-each-call-is-attributed-to)).
2. Decide your fail-mode for unenrolled machines. The sample below sets `ACP_REQUIRE_ENROLLMENT=1`: a seat with no credential blocks each shell command with the enrollment step. Drop the variable to get the self-serve posture instead (run, warn, log).
3. Audit any existing `managed_config.toml`. Its `approval_policy`, `approvals_reviewer`, and `sandbox_mode` fields are now read as requirements, so a stale value locks every developer into that mode.

## Step 1: Put the script in a managed directory

Codex enforces the hook configuration from `requirements.toml` but doesn't distribute the scripts it names, and asks that commands use absolute paths inside `managed_dir`. Your MDM drops two files there. macOS and Linux:

```bash
COMMIT=<sha>            # a commit of github.com/agentic-control-plane/claude-code-acp-plugin
install -d -m 755 /etc/acp
curl -sf "https://raw.githubusercontent.com/agentic-control-plane/claude-code-acp-plugin/$COMMIT/bin/govern.mjs" -o /etc/acp/govern.mjs
echo "<expected-sha256>  /etc/acp/govern.mjs" | shasum -a 256 -c

cat > /etc/acp/codex-hook.sh <<'SH'
#!/bin/sh
ACP_CLIENT=codex ACP_HARNESS=codex ACP_REQUIRE_ENROLLMENT=1 exec node /etc/acp/govern.mjs
SH
chmod 755 /etc/acp/codex-hook.sh
chmod 644 /etc/acp/govern.mjs
```

Windows, into `C:\ProgramData\acp\`: the same `govern.mjs`, plus `codex-hook.cmd`:

```bat
@echo off
set ACP_CLIENT=codex
set ACP_HARNESS=codex
set ACP_REQUIRE_ENROLLMENT=1
node "C:\ProgramData\acp\govern.mjs"
```

Node 18 or newer must be on the PATH of the signed-in user. The script has no other dependency.

## Step 2: The requirements file

```toml
# requirements.toml — admin-enforced; users can't override these values.

# Only hooks from managed layers run. User, project, session, and plugin hooks are skipped.
allow_managed_hooks_only = true

[features]
hooks = true                      # pin on, even for users who disabled hooks locally

[hooks]
managed_dir = "/etc/acp"
windows_managed_dir = 'C:\ProgramData\acp'

[[hooks.PreToolUse]]
matcher = ".*"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "/etc/acp/codex-hook.sh"
command_windows = 'C:\ProgramData\acp\codex-hook.cmd'
timeout = 5
statusMessage = "ACP policy check"

[[hooks.PostToolUse]]
matcher = ".*"
[[hooks.PostToolUse.hooks]]
type = "command"
command = "/etc/acp/codex-hook.sh"
command_windows = 'C:\ProgramData\acp\codex-hook.cmd'
timeout = 5

# MCP allowlist. A server is enabled only when the user's config defines one whose
# identity matches. Do NOT leave this table present-but-empty: that disables all MCP.
[mcp_servers.acp]
identity = { url = "https://api.agenticcontrolplane.com/mcp" }
```

What each block does:

- **`allow_managed_hooks_only`** makes the ACP hook the only hook. Without it, requirements hooks run alongside whatever users add, which is fine for a shadow phase and wrong for enforcement.
- **`[features] hooks = true`** pins the hooks engine on. Hooks are on by default since 0.145, but a user can turn them off locally, and a pinned feature can't be.
- **`[hooks]`** names the managed directories. Both hook commands are absolute paths inside them, per OpenAI's guidance.
- **`matcher = ".*"`** is what the self-serve installer uses. Codex only fires `PreToolUse` for the shell tool today, so this matches everything it will ever send.
- **`[mcp_servers.acp]`** allowlists ACP's endpoint by URL. It doesn't add the server: each user's `~/.codex/config.toml` needs `[mcp_servers.acp]` with `url = "https://api.agenticcontrolplane.com/mcp"`, and Codex signs them in with MCP OAuth on first use. To seed it fleet-wide, put that block in `managed_config.toml` (managed *defaults*, which users may change) rather than in requirements.

## Step 3: Deliver it

Three channels, lowest to highest precedence. Higher layers override scalars and lists from lower ones; hooks compose per OpenAI's field-specific rules.

| Channel | Where | Notes |
|---|---|---|
| System file | `/etc/codex/requirements.toml` (macOS, Linux); `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows) | Root-owned. Your MDM writes it alongside the script. |
| ChatGPT Enterprise workspace | [chatgpt.com/codex/settings/managed-configs](https://chatgpt.com/codex/settings/managed-configs) | A cloud config bundle for users signed in with ChatGPT. Carries requirements only. If the fetch fails with no valid cache, Codex returns an error rather than starting without the layer. |
| macOS MDM profile | Preference domain `com.openai.codex`, key `requirements_toml_base64` | The TOML, base64-encoded with no line wrapping. Jamf Pro, Kandji, Fleet all work. Highest precedence. |

The cloud bundle is the interesting one: an admin with a ChatGPT Enterprise workspace can push the hook *configuration* to every seat with no device management at all. The script still has to exist on disk, so MDM is unavoidable for the hook itself; the MCP entry alone is console-only.

## Step 4: Each person enrolls once

The pushed config identifies no one. Each developer opens [cloud.agenticcontrolplane.com/plugin/authorize](https://cloud.agenticcontrolplane.com/plugin/authorize), signs in with their work identity, and pastes the one line it shows:

```bash
echo 'gsk_…' > ~/.acp/credentials
```

Until they do, with `ACP_REQUIRE_ENROLLMENT=1` every shell command is denied with exactly that instruction in the message. Codex also asks each user to confirm hooks once in `/hooks` on first run; managed hooks are enforced regardless, but the confirmation is where a user sees which hooks are running.

## What you'll see

- Console rows with client `codex`, one per person, with Codex's session source (`cli`, `vscode`, `exec`, `subAgent`) on each row.
- On an unenrolled seat: `[ACP] Not enrolled: this machine runs your organization's ACP policy, but no workspace credential is present…` and the command doesn't run.
- On a policy deny: the rule's reason. On an `ask` rule: a deny carrying the approval link, because Codex's hook parser acts on deny only. After a human approves, the identical re-run passes under the standing grant.

Codex-specific limits are unchanged from the self-serve integration: hooks fire for shell commands only, `updatedInput` is rejected so scoped vendor-token injection is off, and `type = "command"` is the only handler. Details on [the Codex integration page](/integrations/codex#limitations--read-this-before-relying-on-codex-hooks-alone).

## Related

- [Enterprise rollout: the shape all harnesses share](/docs/enterprise)
- [Codex CLI integration](/integrations/codex) — the self-serve install and its limits
- [Codex CLI native controls](/controls/codex-cli) — approvals, Guardian, sandbox, where `requirements.toml` fits
- OpenAI: [Admin rollout guide](https://learn.chatgpt.com/docs/enterprise/admin-setup) · [Managed configuration](https://learn.chatgpt.com/docs/enterprise/managed-configuration)
