Roll ACP out to every seat from the admin console
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 —
requirements.tomlfrom a system file, MDM, or the ChatGPT Enterprise workspace - Claude Code — managed settings from the claude.ai admin console, MDM, or a system file
- Cursor — team hooks from the dashboard, or an enterprise
hooks.json - 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, sign in with your work identity, and paste the line it shows:
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.
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.
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.
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:
# 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 enrolledon its first tool call and the call doesn’t run. Without the flag it shows[ACP] ⚠ UNGOVERNEDand the call runs. - Vendor-side, each harness reports which config layer won:
/statusin Claude Code names the source, Codex notifies the user when a local value was overridden by a requirement, Cursor’s audit log recordsteam_hookandmcp_server_configevents.
Pitfalls
- Pushing before claiming the domain. Enrollments fork into personal workspaces. Claim first, push second.
- Codex’s legacy fields.
approval_policy,approvals_reviewer, andsandbox_modein an oldermanaged_config.tomlare now read as hard requirements. Audit that file before adding hooks to it. - An empty MCP table. In Codex, a present-but-empty
mcp_serverstable in requirements disables every MCP server. In Claude Code, deployingmanaged-mcp.jsonmakes its list the only servers that load. - Allowlist is not install. Every vendor says so. The hook is the enforcement; the MCP entry is additive.
- 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.
Related
- Team setup — the self-serve route, invites, roles, shadow-then-enforce
- Policies & scopes — what the workspace policy can express
- Native controls, compared — what each harness enforces on its own
Run agents? ACP lets you see, control, and price every tool call they make — allow/ask/deny policy, per-session cost, and a full audit log for Claude Code, Cursor, Codex, and OpenClaw, in one command:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Getting started → · see your first governed call → · free up to 5 agents · prefer fully on-device? add --local