# Enterprise Rollout — Push ACP to Every Coding-Agent Seat from the Admin Console

How an enterprise puts one permission policy on every Codex, Claude Code, Cursor, and Gemini CLI seat without anyone running an installer: managed config from the vendor's admin console or MDM, one hook script, one MCP allowlist entry, and per-person enrollment so every call is still attributed to a human.

# Roll ACP out to every seat from the admin console

[Team setup](/docs/team-setup) is for a team that installs itself: invite everyone, each person runs one installer. Past a few dozen seats nobody runs an installer. Configuration arrives through the harness vendor's own admin channel, and the question becomes: *can that channel carry ACP?*

It can, in all four harnesses enterprises standardise on. This page is the shape they share. The per-harness pages have the exact files:

- [Codex](/docs/enterprise/codex) — `requirements.toml` from a system file, MDM, or the ChatGPT Enterprise workspace
- [Claude Code](/docs/enterprise/claude-code) — managed settings from the claude.ai admin console, MDM, or a system file
- [Cursor](/docs/enterprise/cursor) — team hooks from the dashboard, or an enterprise `hooks.json`
- [Gemini CLI](/docs/enterprise/gemini-cli) — system settings and admin controls

## The shape

Three moves, the same in every harness.

**1. Push the hook config.** Managed config declares a pre-tool hook that runs ACP's hook script before every tool call, and a post-tool hook that records the result. Each harness has a switch that stops users from removing or adding hooks: `allow_managed_hooks_only` in Codex, `allowManagedHooksOnly` in Claude Code, enterprise-over-user precedence in Cursor. The pushed config contains no secret and no policy. Policy lives in your ACP workspace and is evaluated per call.

**2. Get the script onto the machine.** The hook script is one dependency-free file. Claude Code fetches it through the plugin marketplace when managed settings force-enable the plugin, so no device management is involved. Codex and Cursor run whatever file the config points at, so your MDM copies it into a managed directory. Vendors are explicit that they don't distribute hook scripts for you.

**3. Allowlist the ACP MCP endpoint.** Every harness lets admins restrict which MCP servers can run. ACP's is one URL, `https://api.agenticcontrolplane.com/mcp`. In every vendor's docs an allowlist entry is permission, not installation; the per-harness pages say what does and doesn't push the server itself.

## Who each call is attributed to

This is the part that separates a fleet rollout from a shared service account.

The hook reads a per-person key from `~/.acp/credentials` in the signed-in user's home directory. Nothing in the managed config identifies a person, so nothing in the managed config has to be per-machine.

**Enrollment is one step per person.** Open [cloud.agenticcontrolplane.com/plugin/authorize](https://cloud.agenticcontrolplane.com/plugin/authorize), sign in with your work identity, and paste the line it shows:

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

From the next session on, every call from that machine arrives under that person's name, with the harness that made it.

**Claim your domain before you push.** On the workspace's Team page, claim your company email domain so that every sign-in from it joins your workspace automatically. Without that, a person who signs in before being invited gets a personal workspace and their calls report to nobody. Public providers such as gmail.com can't be claimed; those people need an [invite](/docs/team-setup#step-1-create-the-workspace-and-invite-the-team--before-anyone-installs).

**What happens before someone enrolls.** The hook's default posture on a machine with no credential is to run the call, print a loud warning once per session, and write a line to `~/.acp/lapse.log`. That's right for a self-serve install and wrong for a fleet an admin believes is covered. Managed rollouts set one variable in the hook command:

| Setting | Machine without a credential |
|---|---|
| default | Call runs. Warning printed once per session. `UNGOVERNED` line in `~/.acp/lapse.log`. |
| `ACP_REQUIRE_ENROLLMENT=1` | Call is **denied** with the enrollment step in the message. `BLOCKED` line in `~/.acp/lapse.log`. Local mode doesn't satisfy it. |

That is the only thing the flag changes. What the hook does when the gateway is unreachable is unchanged and documented on [how a call is decided](/docs/governance-model).

**The MCP path needs no enrollment step.** The ACP MCP server signs people in with MCP OAuth in the browser, so calls through it are attributed the same way, with no file to write.

## Channels, by harness

| Harness | Where the hook config comes from | Who delivers the script | MCP allowlist entry | Console-only, no MDM? |
|---|---|---|---|---|
| Codex | `requirements.toml`: system file, macOS MDM profile, or a cloud bundle from the ChatGPT Enterprise workspace | Your MDM, into the `managed_dir` the config names | `[mcp_servers.acp]` with `identity = { url = "…" }` | Config yes. Script still needs MDM. |
| Claude Code | `managed-settings.json`, macOS profile, Windows registry, or the claude.ai admin console | The plugin marketplace, at startup | `allowedMcpServers` with `serverUrl` | Yes, fully. |
| Cursor | Team hooks in the dashboard (synced every thirty minutes), or an enterprise `hooks.json` | Your MDM | MCP Configuration in the dashboard | Config yes. Script still needs MDM. |
| Gemini CLI | System `settings.json` for hooks (a default users can override), admin controls for MCP | Not applicable: ACP ships no Gemini CLI hook adapter yet | `admin.mcp.config`; `admin.mcp.requiredConfig` pushes the server | MCP yes. Hook not shipped. |

## The script

One file, `govern.mjs`, no dependencies, Node 18 or newer on the machine's PATH. It's the same file the self-serve installer writes to `~/.acp/govern.mjs`, and its source is [public](https://github.com/agentic-control-plane/claude-code-acp-plugin/blob/main/bin/govern.mjs).

For Codex and Cursor your MDM places it in a directory users can't write to. Pin a commit and check the hash rather than pulling `main` at deploy time:

```bash
# Pick a commit of the plugin repo, then in your MDM script:
COMMIT=<sha>
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
chmod 644 /etc/acp/govern.mjs
```

Each per-harness page adds a two-line wrapper next to it that sets the harness name and the enrollment flag, so the managed config points at one absolute path with no quoting.

## Verify it's covering the fleet

- **The console** shows a row per person within their first session, with the harness in the client column. A person with zero rows hasn't enrolled, or enrolled into a personal workspace because the domain wasn't claimed.
- **An unenrolled machine** with the flag set shows `[ACP] Not enrolled` on its first tool call and the call doesn't run. Without the flag it shows `[ACP] ⚠ UNGOVERNED` and the call runs.
- **Vendor-side**, each harness reports which config layer won: `/status` in Claude Code names the source, Codex notifies the user when a local value was overridden by a requirement, Cursor's audit log records `team_hook` and `mcp_server_config` events.

## Pitfalls

1. **Pushing before claiming the domain.** Enrollments fork into personal workspaces. Claim first, push second.
2. **Codex's legacy fields.** `approval_policy`, `approvals_reviewer`, and `sandbox_mode` in an older `managed_config.toml` are now read as hard requirements. Audit that file before adding hooks to it.
3. **An empty MCP table.** In Codex, a present-but-empty `mcp_servers` table in requirements disables every MCP server. In Claude Code, deploying `managed-mcp.json` makes its list the only servers that load.
4. **Allowlist is not install.** Every vendor says so. The hook is the enforcement; the MCP entry is additive.
5. **Two hooks on one machine.** A person who ran the self-serve installer before the push has a user-level hook too. Codex and Claude Code ignore it once managed-only is on. Cursor merges layers, so the same call is checked twice: harmless, but it doubles hook latency. Remove the user entry.

## What's verified and what isn't

The hook script and its wire contract are the ones every self-serve install runs today. The managed-config samples on the per-harness pages follow the vendors' published schemas as of September 2026, linked from each page. We have not yet exercised a full rollout on an enterprise-plan seat of any of the four; the issue tracking that is [gatewaystack-connect #943](https://github.com/davidcrowe/gatewaystack-connect/issues/943).

## Related

- [Team setup](/docs/team-setup) — the self-serve route, invites, roles, shadow-then-enforce
- [Policies & scopes](/docs/policies) — what the workspace policy can express
- [Native controls, compared](/controls) — what each harness enforces on its own
