Enforcing one policy across every coding agent you run
A managed agent policy is one set of organization rules (sign-in, models, permissions, MCP servers, plugins, hooks, versions) written once, rendered into each tool’s enforcement file: managed-settings.json for Claude Code and GitHub Copilot, requirements.toml for Codex, and Cursor’s admin settings. Developers cannot override what those files enforce; controls a tool cannot express (listed per tool below) need a compensating check.
Security approved Claude Code in March with an MCP allowlist. Since then the platform team moved to Codex, the frontend team runs Cursor, and Copilot turned on its Claude and Codex agents. Four tools read four config files, and nobody can say which MCP servers a given laptop allows today.
This page is for the CTO who owns agent policy and the tech lead who rolls it out. It is step 11 of the CTO track. It follows agent identity, credentials and secrets, whose deny rules and MCP scopes become managed settings here, and it leads to the agent platform team, which runs this policy as a product.
What one managed policy across vendors gives you
Section titled “What one managed policy across vendors gives you”- A ten-control baseline, written as intent rather than as one vendor’s keys.
- How Claude Code, Codex, Cursor and GitHub Copilot deliver and verify managed policy, and how to govern cloud agents.
- Policy files checked against Claude Code 2.1.283, Codex 0.157.1 and GitHub’s Copilot docs on 2026-09-26.
- A policy repository, a CI allowlist parity check, a canary rollout, verification steps and failure modes.
What should the policy say before you touch any config?
Section titled “What should the policy say before you touch any config?”Write the intent first, in one file your security lead signs. Vendor keys change from release to release; the intent changes only when your risk appetite does. This baseline is moderate: it blocks the ways a laptop agent leaks data or escapes review, and leaves day-to-day permission prompts to teams, as described in permissions and sandboxing for agents.
| # | Control | Baseline rule | Why it is org-level, not team-level |
|---|---|---|---|
| 1 | Sign-in | Company account or organization only | Personal accounts bypass data terms and audit |
| 2 | Models | Only models your data terms cover; default set centrally | Any other model is a data-handling breach |
| 3 | Permission floor | Bypass or “allow all” modes are off on developer machines | One flag removes every other control |
| 4 | Secret reads | Agents cannot read .env* or secrets/** | The threat model needs secrets out of context |
| 5 | MCP servers | Only approved servers load, matched by URL or exact command | An unvetted server is code with your tokens |
| 6 | Plugins and marketplaces | Only the official marketplace and your own | One plugin bundles hooks, MCP servers and skills |
| 7 | Command rules | Destructive commands, such as force-push, are forbidden | Cheap to enforce, expensive to undo |
| 8 | Minimum version | Clients below the approved version refuse to start | Security fixes and policy keys arrive in new versions |
| 9 | Telemetry | Usage events go to your collector | Audit and cost reports need one data source |
| 10 | Cloud agents | Agent PRs pass the same required checks as humans | Their policy lives at the Git host |
Controls 3 and 4 are not optional for any tool. Controls 5 and 6 drift most, because each tool configures MCP and plugins its own way. To vet what goes on the MCP list, see MCP registries and gateways.
How does each tool enforce managed policy?
Section titled “How does each tool enforce managed policy?”Claude Code and GitHub Copilot share most of one managed-settings.json dialect, including permissions.deny, disableBypassPermissionsMode, strictKnownMarketplaces and allowedMcpServers. Codex uses a separate TOML constraints file.
| Claude Code (2.1.283) | Codex (0.157.1) | Cursor | GitHub Copilot | |
|---|---|---|---|---|
| Policy file | managed-settings.json (plus managed-settings.d/*.json, managed-mcp.json) | requirements.toml | Admin dashboard | managed-settings.json in .github-private/copilot/ |
| Delivery | claude.ai admin console (server-managed), MDM (the com.anthropic.claudecode managed preferences domain on macOS, HKLM\SOFTWARE\Policies\ClaudeCode on Windows, per Anthropic’s managed settings docs), or a system file | Workspace-delivered layer, macOS MDM, or /etc/codex/requirements.toml | team and mdm setting sources exist (@cursor/sdk 1.0.32) | Server-managed from .github-private, MDM, or a system file (MDM: macOS and Windows only) |
| File path (Linux) | /etc/claude-code/managed-settings.json | /etc/codex/requirements.toml | — | /etc/github-copilot/managed-settings.json |
| Several sources | First source with a policy key wins by default; managedSourcesBehavior: "merge" composes them | Sources compose; /debug-config shows which one set each constraint | — | MDM > server-managed > file > user, per key; permissions.deny/ask/allow and sandbox compose most-restrictively; allowedMcpServers is the intersection |
| Sign-in | forceLoginMethod, forceLoginOrgUUID | allowed_login_methods + allowed_chatgpt_workspaces | SSO (confirm) | forceLoginOrgs (MDM or system file) |
| Models | availableModels + enforceAvailableModels; deniedModels (v2.1.283, latest channel) | model_provider pin; [models.new_thread] sets a default | Reported: Router model allow and block lists; Privacy Mode gates retention-requiring models (confirm) | AI Controls: enable, disable or delegate each model; model key sets a default only |
| Permission floor | permissions.disableBypassPermissionsMode, permissions.disableAutoMode | allowed_approval_policies, allowed_sandbox_modes | Reported: agent permissions per group (confirm) | permissions.disableBypassPermissionsMode |
| Telemetry | env with OTEL_* variables | Not a requirements key; [otel] in a distributed config.toml or MDM | Not verified | telemetry (CLI, VS Code, JetBrains) |
| MCP allowlist | allowedMcpServers + allowManagedMcpServersOnly | [mcp_servers.<name>.identity] | Not verified | allowedMcpServers / deniedMcpServers |
| Plugins and hooks | strictKnownMarketplaces, blockedMarketplaces; allowManagedHooksOnly | [marketplaces] with restrict_to_allowed_sources; allow_managed_hooks_only | Reported: team marketplace with plugins set Default On or Required (confirm) | strictKnownMarketplaces, enabledPlugins; hooks are not a managed key |
| Per-group policy | Separate files, or a Claude apps gateway per IdP group | One workspace layer; separate files per device group | Reported: Organizations › Teams › Groups (confirm) | Team overrides with overridable and team-mappings.json |
| Check on a machine | /status → Setting sources; claude doctor | /debug-config in the TUI | Admin dashboard | Validator on the AI Controls Agents tab |
Write the policy file for each tool
Section titled “Write the policy file for each tool”Each tab holds one file implementing every control that tool can express, with the gaps listed below it. The MCP allowlist is the same in every tool: GitHub’s and Sentry’s remote servers and Playwright’s local one. Pin the version you vetted; @latest approves whatever ships next. Replace the organization UUID, marketplace repository and collector URL with your own.
Deploy this through the claude.ai admin console, MDM, or the system path: /Library/Application Support/ClaudeCode/ on macOS, /etc/claude-code/ on Linux and WSL, and C:\Program Files\ClaudeCode\ on Windows.
{ "forceLoginMethod": "claudeai", "forceLoginOrgUUID": ["00000000-0000-0000-0000-000000000000"], "availableModels": ["opus", "sonnet"], "enforceAvailableModels": true, "permissions": { "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"], "disableBypassPermissionsMode": "disable" }, "allowedMcpServers": [ { "serverUrl": "https://api.githubcopilot.com/*" }, { "serverUrl": "https://mcp.sentry.dev/*" }, { "serverCommand": ["npx", "@playwright/mcp@0.0.83"] } ], "allowManagedMcpServersOnly": true, "strictKnownMarketplaces": [ { "source": "github", "repo": "anthropics/claude-plugins-official" }, { "source": "github", "repo": "acme/agent-plugins" } ], "disableSideloadFlags": true, "requiredMinimumVersion": "2.1.274", "env": { "CLAUDE_CODE_ENABLE_TELEMETRY": "1", "OTEL_METRICS_EXPORTER": "otlp", "OTEL_LOGS_EXPORTER": "otlp", "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.acme.internal:4317" }}What the non-obvious keys do:
availableModelsalone leaves the Default option on the account’s runtime default;enforceAvailableModelsmakes Default obey the list. Themodelkey is only an initial selection.allowManagedMcpServersOnlystops users from widening the list. Once oneserverUrlentry exists, every remote server must match a URL pattern. AserverNameentry is never a security control, because users pick the name.disableSideloadFlagsrejects--plugin-dir,--plugin-url,--agentsand--mcp-configat startup (v2.1.193 or later), so CI runners that pass--mcp-configneed their own policy file.OTEL_EXPORTER_OTLP_PROTOCOLis required: Claude Code has no default OTLP protocol, so without it nothing is exported and control 9 fails silently. Port4317is gRPC; usehttp/protobuffor a4318endpoint.requiredMinimumVersionis2.1.274, thestablechannel on 2026-09-26. It blocks older binaries at startup only.- For a fixed MCP set users cannot extend, deploy
managed-mcp.jsoninstead, through MDM or the system path; the admin console cannot deliver it.
The stricter tier adds allowManagedPermissionRulesOnly and allowManagedHooksOnly. Both switch off project-level rules and hooks that teams use as quality gates, so reserve them for regulated data.
Codex reads admin constraints from requirements.toml, which is separate from the config.toml defaults users edit. On Linux the system file is /etc/codex/requirements.toml. Codex also accepts a workspace-delivered enterprise layer and macOS MDM managed preferences, and composes them.
# /etc/codex/requirements.toml (agent-policy v1.4)allowed_login_methods = ["chatgpt"]allowed_chatgpt_workspaces = ["CHATGPT_WORKSPACE_ID"]allowed_approval_policies = ["on-request", "untrusted"]allowed_sandbox_modes = ["read-only", "workspace-write"]
[mcp_servers.github.identity]url = "https://api.githubcopilot.com/mcp/"
[mcp_servers.sentry.identity]url = "https://mcp.sentry.dev/mcp"
[mcp_servers.playwright.identity]command = { executable = "npx", args = [{ match = "exact", value = "@playwright/mcp@0.0.83" }] }
[marketplaces]restrict_to_allowed_sources = true
[marketplaces.allowed_sources.acme]source = "git"url = "https://github.com/acme/agent-plugins.git"
[rules]prefix_rules = [ { pattern = [{ token = "git" }, { token = "push" }, { token = "--force" }], decision = "forbidden", justification = "Force-push is for humans" },]What the Codex source code (0.157.1) says about these keys:
allowed_login_methods = ["chatgpt"]blocks API-key sign-in, andallowed_chatgpt_workspaceslimits ChatGPT sign-in to your workspace ID (control 1).- The first entry of
allowed_approval_policiesis the fallback Codex uses when a user or flag asks for a disallowed policy; put the policy you want people to land on first. In 0.157.1, users can no longer selectuntrustedinconfig.toml, but Codex still applies it to projects marked untrusted. Keep it in the list, second, so those projects are not widened to the fallback. allowed_sandbox_modesmust includeread-only, or Codex rejects the requirement. It constrains permission profiles too.- MCP requirements match on the server name and its identity together: a GitHub server registered as
ghinstead ofgithubis disabled even though its URL is approved. Publish the exact names with the list. identity.command = "npx"as a plain string matches anynpxserver. Use the table form withexecutableandargs, as above; the arguments must match in number and order.- Requirement rules accept only
promptorforbidden; Codex keeps the most restrictive result, soallowis refused. allow_managed_hooks_only = trueignores user, project and session hooks, and works only inrequirements.toml.
requirements.toml in 0.157.1 has no allowed_models-style list (checked against config_requirements.rs on 2026-09-26). model_catalog_json pins the catalog file Codex reads; test it before treating it as an allowlist. Control model access through your ChatGPT workspace settings or a gateway, as described in where the model runs.
There is no key for control 8 or 9 either. Pin the Codex version with package or MDM tooling, and ship an [otel] section in the config.toml you distribute; users can edit it, so watch the collector for missing machines.
Cursor’s team controls live in its admin dashboard. Because cursor.com was unreachable on 2026-09-26, this tab lists questions to close there rather than keys to paste.
| Baseline control | What to confirm in Cursor’s admin settings | Evidence to record |
|---|---|---|
| 1 Sign-in | SSO is enforced; no personal accounts on company repositories | Dated SSO screenshot |
| 2 Models | Reported: Router admin model allow and block lists per team or group; Privacy Mode enforced org-wide, with admin approval for models that need data retention. Confirm both | Model list and Privacy Mode setting |
| 3, 4 Permission floor, secret reads | Reported: Groups carry agent permissions. Confirm which run mode members can choose, whether it can be locked, and how .env files are excluded from context | Setting plus a test session log |
| 5 MCP servers | Whether MCP servers can be restricted to an approved list at team level | The list and a blocked-server test |
| 6 Plugins | Reported: team marketplace admins can set plugins Default On or Required. Confirm whether other sources can be blocked | Marketplace configuration |
| 8 Version | Whether a minimum client version can be enforced | Setting or the MDM configuration |
“Reported” means found in Cursor docs or changelog excerpts (secondary, 2026-09-26) and not opened here. Record each answer with the date and Cursor version in your policy repository. The Cursor privacy and security page covers the data-handling side.
Copilot’s enterprise managed settings use managed-settings.json in a .github-private repository that you select as the enterprise’s source of client governance. GitHub validates the file and shows errors on the Agents tab of AI Controls. Supported clients receive changes within about an hour.
{ "model": "auto", "permissions": { "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"], "disableBypassPermissionsMode": "disable" }, "allowedMcpServers": [ { "serverUrl": "https://api.githubcopilot.com/*" }, { "serverUrl": "https://mcp.sentry.dev/*" }, { "serverCommand": ["npx", "@playwright/mcp@0.0.83"] } ], "strictKnownMarketplaces": [ { "source": "github", "repo": "acme/agent-plugins" } ], "telemetry": { "enabled": true, "endpoint": "https://otel.acme.internal:4318", "protocol": "http/protobuf", "captureContent": false, "lockCaptureContent": true }}Save it as copilot/managed-settings.json. The telemetry block implements control 9 in Copilot CLI, VS Code and JetBrains, not in the GitHub Copilot app or Copilot cloud agent; protocol accepts http/json or http/protobuf. Outside this file:
- Sign-in. Copilot CLI’s
forceLoginOrgspins sign-in to your organizations; deliver it by MDM or the system file, not.github-private, because it must apply before the first sign-in. It fails closed: an unreadable value blocks all sign-in. - Models. Availability is set in AI Controls, per model. Check the Default availability for released models policy, which otherwise gives new models to everyone. Per repository, Copilot CLI also reads
.github/allowed_models.txt(one glob per line plus onefallback:line); it does not filter bring-your-own-key models. - MCP policy. The MCP servers in Copilot policy must be enabled for any server to run. If you restricted MCP to a registry before, GitHub recommends setting Restrict MCP access to registry servers to Allow all, so the allowlist is the one source of truth.
Per-team exceptions use { "overridable": VALUE } in the main file, a team file under copilot/teams/, and copilot/team-mappings.json. On macOS and Linux, Copilot CLI rejects a managed settings file that is a symbolic link, not owned by root, or group- or world-writable.
How do you govern cloud agents that only open pull requests?
Section titled “How do you govern cloud agents that only open pull requests?”Copilot cloud agent, the Claude and Codex agents inside Copilot, Jules and Devin run on remote machines, so nothing deployed to a laptop reaches them. A Claude Code cloud session likewise reads only server-managed settings. Govern these agents where their work lands: the Git host.
| Control | Where it lives | Applies to |
|---|---|---|
| Which agents may run | Copilot policies in AI Controls, per agent; the repositories each vendor’s GitHub app can access | All of them |
| What can merge | Rulesets or branch protection: required reviews, required status checks, no bypass for bot identities | Every agent that opens a PR |
| Which secrets a run sees | Environment and repository secrets scoped per workflow; Jules’s GitHub Action reads its key from a repository secret, JULES_API_KEY | Agents started from GitHub Actions |
| What was done | The enterprise audit log, including agentic events in AI Controls; your CI logs | Copilot agents; every agent through CI |
Jules’s and Devin’s admin documentation could not be reached on 2026-09-26, so this page makes no claim about their in-product settings. The Git-host boundary works for any vendor, which is why control 10 reads “the same required checks as humans”. The evidence bundle makes a PR mergeable without a human reading every line.
How do you distribute and version one policy across vendors?
Section titled “How do you distribute and version one policy across vendors?”Treat the policy like any other production configuration: one repository, reviewed changes, CI gates and a staged rollout.
-
Create the policy repository. One directory per tool, rendered from the intent table.
agent-policy/├── POLICY.md # the ten-control intent table, signed by security├── CHANGELOG.md # one entry per version: what changed and why├── CODEOWNERS # security + platform team must approve├── claude-code/managed-settings.json├── claude-code/ci/managed-settings.json # CI runners: no disableSideloadFlags├── codex/requirements.toml├── copilot/managed-settings.json # synced to .github-private/copilot/├── cursor/EVIDENCE.md # the dated answers from the Cursor tab└── tests/mcp_parity.py -
Gate every change in CI. Parse every file, then fail when the MCP allowlists disagree. This script, run against the three files on this page, exits
1when Claude Code and Copilot differ or when Codex and Claude Code disagree in either direction."""Fail CI when the MCP allowlists disagree across tools."""import fnmatchimport jsonimport sysimport tomllibclaude = json.load(open("claude-code/managed-settings.json"))copilot = json.load(open("copilot/managed-settings.json"))with open("codex/requirements.toml", "rb") as f:codex = tomllib.load(f)def allowlist(settings):entries = settings.get("allowedMcpServers", [])urls = {e["serverUrl"] for e in entries if "serverUrl" in e}cmds = {tuple(e["serverCommand"]) for e in entries if "serverCommand" in e}return urls, cmdsproblems = []if allowlist(claude) != allowlist(copilot):problems.append(f"Claude Code and Copilot differ: {allowlist(claude)} vs {allowlist(copilot)}")codex_urls, codex_cmds = {}, {}for name, req in codex.get("mcp_servers", {}).items():ident = req.get("identity", {})if isinstance(ident.get("url"), str):codex_urls[ident["url"]] = namecmd = ident.get("command")if isinstance(cmd, dict):argv = (cmd["executable"], *(a.get("value", "") for a in cmd.get("args", [])))codex_cmds[argv] = nameclaude_urls, claude_cmds = allowlist(claude)for url, name in codex_urls.items():if not any(fnmatch.fnmatch(url, p) for p in claude_urls):problems.append(f"Codex allows {name} at {url}; Claude Code does not")for argv, name in codex_cmds.items():if argv not in claude_cmds:problems.append(f"Codex allows {name} as {' '.join(argv)}; Claude Code does not")for pattern in claude_urls:if not any(fnmatch.fnmatch(url, pattern) for url in codex_urls):problems.append(f"Claude Code allows {pattern}; Codex has no matching server")for argv in claude_cmds - codex_cmds.keys():problems.append(f"Claude Code allows {' '.join(argv)}; Codex has no matching server")print("\n".join(problems) or "MCP allowlists agree across Claude Code, Codex and Copilot")sys.exit(1 if problems else 0)Run it with
python3 tests/mcp_parity.py(Python 3.11 or later, fortomllib) as a required check, next topython3 -m json.toolon each JSON file. -
Roll out to a canary group first. Deliver the new version to the platform team’s machines for two working days. Server-managed changes arrive within about an hour in Claude Code and Copilot; MDM is checked every 30 minutes by Claude Code and hourly by Copilot; a file change needs a Copilot restart.
-
Promote to everyone and pin the version. Tag the release (
agent-policy v1.4) and put the version string in a comment at the top of each file, so a support request can name it. -
Announce removals first. A new deny rule or a removed MCP server breaks someone’s workflow; post the changelog entry before the rollout and name the replacement.
The platform team owns the repository and the rollout; the security lead approves every change to POLICY.md and signs off the quarterly audit. The operating model has the full RACI.
How do you prove the policy is in force?
Section titled “How do you prove the policy is in force?”A config file proves only that someone wrote it. Prove enforcement on real machines, on a schedule.
- Read the effective source on a sample of machines. In Claude Code,
/statusmust showEnterprise managed settingswith the source you deployed, such as(remote)or(file);Skipped sourcesmeans a higher-ranked source won, andclaude doctorlists entries dropped as invalid. In Codex,/debug-configshows which source set each constraint. For Copilot, the Agents tab validator must show no issues. - Run negative tests. Add an unlisted MCP server on a canary machine and confirm each tool refuses it. Start Claude Code with
--dangerously-skip-permissions, which it must reject, and start Codex with-a never, then confirm that it prints a startup warning and that/debug-configshowson-request. - Watch the audit stream. Claude Code emits
claude_code.plugin_installedandclaude_code.plugin_loadedevents to your OpenTelemetry collector; third-party plugin names are redacted unless you setOTEL_LOG_TOOL_DETAILS=1. Copilot’s enterprise audit log records agentic events. Route both to the pipeline described in agent observability. - Audit quarterly. Compare the repository, the delivered settings and the negative tests; an unexplained difference is a finding with an owner and a date.
Copy-paste prompts for managing agent policy
Section titled “Copy-paste prompts for managing agent policy”What breaks when you enforce one policy across vendors?
Section titled “What breaks when you enforce one policy across vendors?”The Claude Code policy file is silently ignored. You deployed the file by MDM, and someone also set one key in the admin console. By default Claude Code uses only the highest-ranked source that delivers a policy key. Recovery: read Skipped sources in /status, then use one source or set managedSourcesBehavior to "merge" (v2.1.242 or later).
A malformed file locks people out. Claude Code refuses to start when a managed file is not valid JSON; Copilot treats a malformed allowlist as an empty one, which blocks every non-built-in MCP server. Recovery: roll back to the previous tag; the CI parse step prevents a repeat.
Codex disables an approved server. Its name differs from the requirement key, or it was registered with npx -y against an identity without -y. Recovery: publish the exact codex mcp add command for each approved server, with the names and argv from requirements.toml (checked against codex mcp add --help, 0.157.1):
codex mcp add github --url https://api.githubcopilot.com/mcp/codex mcp add sentry --url https://mcp.sentry.dev/mcpcodex mcp add playwright -- npx @playwright/mcp@0.0.83CI jobs break after the rollout. disableSideloadFlags rejects --mcp-config. codex exec asks for approval policy never; if the list omits it, Codex starts with a startup warning and falls back to the first allowed policy, so a headless run gets approval requests nobody can answer (checked in codex-cli 0.157.1). Recovery: give CI runners their own policy file whose allowed_approval_policies includes never, and keep those runners on the hardened workflows from the previous step.
Copilot agents ignore the MCP allowlist. GitHub marks allowedMcpServers, deniedMcpServers and permissions.deny as unsupported on Copilot cloud agent, and the Claude and Codex agents inside Copilot have their own policies. Recovery: configure cloud agent MCP servers per repository or in enterprise custom agent profiles, and review each agent’s row in AI Controls.
New models appear for everyone. Copilot’s Default availability policy and Claude Code’s availableModels without enforceAvailableModels both let unlisted models through. Recovery: set both deliberately and check the model list in the quarterly audit; the models hub lists what is current.
Long-running sessions lag the rollout. requiredMinimumVersion only blocks new starts. Recovery: announce a restart window, then check versions in telemetry.
Where to go next with managed agent policy
Section titled “Where to go next with managed agent policy”- Agent identity, credentials and secrets: the previous CTO-track step, source of the deny rules and MCP scopes.
- The agent platform team: the next step, where this repository becomes a product with an owner.
- MCP registries and gateways: vetting and hosting allowlisted servers.
- Running a team plugin marketplace: the marketplace your policy points to.
- Permissions and sandboxing for agents: the per-repository settings under this floor.
- An AI usage policy engineers will follow: the human-readable policy behind this configuration.
- Tool-specific rollout detail: Claude Code for teams, Codex enterprise governance, and GitHub Copilot compared.