CCLearn

Security review for a team rollout: managed policy, allowManagedHooksOnly, allowlisted hook URLs

verified against Claude Code 2.1.261 · 2026-09-0920 min

By the end you can

  • Choose a managed settings delivery mechanism and justify it against the tampering risk
  • Predict which of two managed sources on one machine is read and which is silently ignored
  • Predict what allowManagedHooksOnly disables beyond hooks
  • Restrict which marketplaces and HTTP hook endpoints a team can reach
  • Prove a policy is in force instead of assuming it because nothing errored

The pilot from Lesson 12.3 worked, and somebody in security has now heard about it. The question you get is never "is Claude Code safe". It is: what stops a developer running a hook that posts our source tree to an endpoint we have never heard of, and how would you know if one already did.

Lesson 12.1 drew the line at what a committed file can do: it reaches a teammate only once they trust the folder, and it is a file any of them can edit. This lesson is the layer above — the one a developer cannot edit back out — and the honest accounting of how far that layer reaches.

What a policy is for, and what it cannot do

Start here, because a security review that begins with the key list ends with a control everyone believes in and nobody tested.

Server-managed settings provide centralized policy enforcement, but they operate as a client-side control, not a security boundary. On unmanaged devices, a user doesn't need admin or sudo access to bypass them. — Server-managed settings

That sentence is about the server-managed mechanism specifically, and the same page's table shows what "bypass" means concretely: a modified client can bypass any client-side control; versions predating the feature never fetch it; and a session pointed at a third-party model provider — CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY, CLAUDE_CODE_USE_MANTLE, CLAUDE_CODE_USE_ANTHROPIC_AWS, or a non-default ANTHROPIC_BASE_URL — bypasses server-managed settings entirely.

The managed-settings page names four more things that sit outside the rule that nothing a developer sets overrides a managed value. All four belong in the review:

What sits outside the ruleWhat it means for the review
A managed model is a default, not a lock--model and ANTHROPIC_MODEL still pick the model for that session; deploy availableModels to restrict the choice
Local admin rightsA developer who is an administrator on the machine can edit the managed source itself — which is why MDM tooling redeploys the profile or file on a schedule
The server-managed cacheAn edit to the local cache lasts until the next successful fetch. Not forever, but not zero either
Other toolsManaged settings bind Claude Code only. A developer who calls the API from another tool is not under them

And one that gets written into policy documents as an egress control when it is not one:

Permission rules and sandboxing cover different layers. Denying WebFetch blocks Claude's fetch tool, but if Bash is allowed, curl and wget can still reach any URL. Sandboxing closes that gap with a network domain allowlist enforced at the OS level. — Set up Claude Code for your organization

deny: WebFetch is a real control over one tool. Written on a slide as "the agent cannot reach the internet", it is false, and the person who wrote it will not find out from Claude Code.

Choosing a delivery mechanism

Claude Code checks four sources in priority order.

MechanismWherePriorityWhat the docs say about tampering
Server-managedclaude.ai admin console, or a self-hosted Claude apps gatewayHighestA tampered cache applies at startup; the next successful fetch restores the policy
plist / registry policymacOS com.anthropic.claudecode; Windows HKLM\SOFTWARE\Policies\ClaudeCodeHighResists it — these require admin privileges to write
File-based managed/Library/Application Support/ClaudeCode/, /etc/claude-code/, C:\Program Files\ClaudeCode\MediumNothing. The docs make the admin-privileges claim for the plist and HKLM only
Windows user registryHKCU\SOFTWARE\Policies\ClaudeCodeLowestWritable without elevation

Do not lend the file row the plist row's guarantee because its path looks system-owned. And read the last row twice:

The plist and HKLM registry locations work with any provider and resist tampering because they require admin privileges to write. The Windows user registry at HKCU is writable without elevation, so treat it as a convenience default rather than an enforcement channel. — Set up Claude Code for your organization

A policy delivered through HKCU is a suggestion with a serious-sounding path, and Claude Code reads it only when nothing above it delivers a policy key.

The choice is mostly determined by two facts. If your devices are enrolled in MDM, endpoint-managed settings give stronger guarantees, because the file can be protected from user modification at the OS level. But endpoint-managed settings do not reach cloud sessions in Anthropic-hosted environments, so an organization using Claude Code on the web should configure server-managed settings as well; both surfaces means both mechanisms. Server-managed settings need a Claude for Teams or Enterprise plan and an Owner or Primary Owner to configure, and are fetched at startup and polled hourly during the session. As Lesson 12.3 established, the claude.ai console applies one policy to the whole organization and cannot target a group — a self-hosted gateway is the documented per-group path.

How managed values combine: first-wins, then merges and locks

This is where reviews go wrong, because the default is not what people picture.

Under the default managedSourcesBehavior of "first-wins", Claude Code uses the highest-ranked source that delivers at least one policy key and ignores the rest — it does not merge them. It shows no warning for the sources it skips. A policy key is any key other than the two control keys managedSourcesBehavior and wslInheritsWindowsSettings, so an MDM profile carrying your hook lockdown is silently inert on any machine that also receives server-managed settings with a single real key in them. A few cross-source keys are exempt and are read from every admin source, including the sandbox locks, forceRemoteSettingsRefresh, and — on v2.1.223 or later — env merged per variable.

Setting managedSourcesBehavior to "merge" applies every admin source, combined by kind of key. It requires v2.1.242 or later, and Claude Code reads the key only from the highest-ranked source carrying either it or a policy key, so a lower source cannot opt itself into merging with the source above it:

Kind of keyHow it combinesExamples
ListsEntries from every sourcepermissions.allow, hooks, sandbox.network.allowedDomains
LocksThe strictest value any source sets; a looser value applies only from the highest-ranked sourceallowManagedHooksOnly, permissions.disableBypassPermissionsMode
Restriction allowlistsTaken whole from the highest source that sets it, no additions from belowavailableModels, allowedMcpServers, strictKnownMarketplaces
Highest-ranked source onlyIgnored in every lower source, even when the top one leaves it unsetapiKeyHelper, forceLoginOrgUUID, permissions.defaultMode
Everything elseThe value from the highest source that sets itmodel, cleanupPeriodDays

Note what merging costs you. Under "merge", a lower source's list entries — permissions.allow rules and hooks — are added to the policy, so the docs are explicit that you turn it on only when every source below your highest one is under an administrator's control.

Below the managed tier, the direction is one-way and useful: list keys such as permissions.allow and permissions.deny merge entries across files, so developers can extend managed lists but not remove from them — unless you set allowManagedPermissionRulesOnly, which makes Claude Code ignore permission rules from user, project and local files and from --allowedTools entirely. Four model keys are the exception to the merge: fallbackModel, modelPicker, availableModels and modelSettings are taken whole from the highest-precedence source that sets them. And a handful of security keys run the other way entirely: Claude Code honours a stricter value from a lower scope, so a project setting disableClaudeAiConnectors: true stands even when a managed source says false.

Locking hooks down with allowManagedHooksOnly

A hook is an arbitrary command that runs on somebody else's machine at an event you do not control. Level 9 taught how to write one; this is the key that decides who is allowed to. Its effects go well past hooks:

  • Your user, project, local, and plugin hooks are blocked. Hooks from plugins force-enabled in managed settings enabledPlugins are exempt — Hooks

The same list continues with three consequences nobody predicts from the name:

  • Your statusLine, fileSuggestion and subagentStatusLine settings are narrowed to managed settings — all three are command settings, so they travel with the hooks.
  • Plugins with a command source are disabled, including ones force-enabled in managed enabledPlugins, unless disableCommandPluginSources is explicitly false. Command sources need v2.1.229 or later to exist at all.
  • Marketplace headersHelper commands are blocked under the same condition (v2.1.238 or later), except for a marketplace managed settings themselves declare.

The settings reference adds two more and one carve-out: hooks declared in agent frontmatter are blocked too; /goal cannot run at all while the key is set, because it depends on hooks; and hooks the Agent SDK registers in process keep running, so an SDK session is not covered by this key.

So the honest description is: it turns off the event-time and UI commands a non-managed source configured. It is not a general ban on commands Claude Code runs — apiKeyHelper, otelHeadersHelper and MCP server commands are governed by their own keys. Deploy it without the exemption for the plugin from Lesson 12.2 in managed enabledPlugins and you have also switched off the team's formatter hook, their status line, and possibly their plugin.

Two more facts belong in the review. Hook entries merge across settings levels rather than replacing each other, so a managed hook cannot be displaced by a developer adding their own; and disableAllHooks set outside managed settings cannot disable managed hooks. A managed hook is the part of this a developer cannot switch off from a file they control — only disableAllHooks in managed settings turns it off.

Allowlisting HTTP hook endpoints

allowManagedHooksOnly is blunt. The narrower control answers the actual question security asked — which endpoints can a hook talk to:

  • allowedHttpHookUrls: when defined at any settings level, Claude Code runs an HTTP hook handler only if its URL matches the merged allowlist — Hooks

Three things hide in that sentence. It is not managed-only, so a team can adopt it in a committed project file before an admin ever gets involved. The allowlist applies to hooks from every source, managed ones included, so your own policy's HTTP hooks have to be on their own list. And merged is doing work: the key's scope is Any file and the arrays merge across settings files, so a developer can widen the allowlist by adding a pattern to ~/.claude/settings.json. Its companion httpHookAllowedEnvVars is stricter in shape — a hook may interpolate a variable into a header only if both its own allowedEnvVars and this key name it — but merges the same way. Both default to unset, which means unrestricted.

Restricting marketplaces

Lesson 12.2 made a plugin the distribution mechanism for the whole conventions pack. That mechanism is also how third-party code arrives, and the managed controls are strictKnownMarketplaces (an allowlist of sources users may add and install from) and blockedMarketplaces (a blocklist). Because they are set in managed settings, individual users and project configurations cannot override them.

json
{
  "strictKnownMarketplaces": [
    { "source": "github", "repo": "acme-corp/approved-plugins" },
    { "source": "hostPattern", "hostPattern": "^git\\.acme\\.internal$" }
  ]
}

The enforcement is stronger than a check at install time:

Restrictions are checked before any network or filesystem operation. The check runs on marketplace add and on plugin install, update, refresh, and auto-update. If a marketplace was added before the policy was configured and its source no longer matches the allowlist, Claude Code refuses to install or update plugins from it. — Plugin marketplaces

Retroactive, in other words: a marketplace someone added last quarter stops receiving updates the day your policy lands. That is the behaviour you want and the support ticket you should expect. The same enforcement applies to blockedMarketplaces.

Three mistakes to avoid. First, strictKnownMarketplaces restricts what users can add; it does not register anything. To make an approved marketplace available automatically, add it to extraKnownMarketplaces in the same file — and note from Lesson 12.1 that extraKnownMarketplaces in a project file waits for each teammate to trust the folder. An empty array is not "no policy yet": it is a complete lockdown that blocks the official Anthropic marketplace too.

Second, the allowlist matches most sources exactly, and treats URLs differing only by a trailing slash, a .git suffix, or the ssh:// versus https:// scheme as different values. If your internal marketplace can be cloned by more than one URL form, use a hostPattern entry instead of a literal URL, or you will ship an allowlist that blocks half your own team.

Third, and this is the one a reviewer should ask about: the allowlist matches the marketplace a plugin comes from, not the entries inside it, so a user can still install a plugin with a command source — a marketplace-declared command that runs on their machine — from an allowed marketplace. disableCommandPluginSources closes that. And the allowlist itself is bypassable for a single run with --plugin-dir, --plugin-url, --agents or --mcp-config until you set disableSideloadFlags (v2.1.193 or later), which rejects those flags at startup.

The dialog your developers will see — if you use the console

The security approval dialog is a server-managed mechanism. Everywhere the docs describe it, the trigger is a setting the client fetched: certain delivered settings require explicit approval before Claude Code applies them in an interactive session — settings that execute shell commands (apiKeyHelper, statusLine, otelHeadersHelper), sandbox binary paths, sandbox settings that weaken isolation or let the proxy read traffic (v2.1.251 or later), some custom env variables — and any hook definition.

When these settings are present, users see a security dialog explaining what is being configured. Users must approve to proceed. If a user rejects the settings, Claude Code exits. — Server-managed settings

The dialog is a review-before-accept moment, and it is worth exactly as much as the attention the person gives it. Lesson 10.4 named the reflex from the other side: unexplained friction gets routed around rather than understood. A team that has been told nothing gets a security dialog on Monday morning and does one of two things — rejects it and cannot work, or learns that the correct response to a Claude Code security dialog is approve. The second is the expensive one, because you have just trained the population you were protecting. Both are worse than a message on Friday saying what will appear, who deployed it, and what it configures.

The docs describe no equivalent dialog for an MDM profile or a managed-settings.json. Deploy the same hook that way and there is no moment at all — your Friday message is the entire communication, not a warm-up for one.

Details that shape the rollout: approval is recorded in the config directory and re-shown when the approving settings change or a different account signs in to the organization; claude install and claude update do not show the dialog and run with the last-approved settings (v2.1.246 or later, and not when the startup waits for the fetch, as forceRemoteSettingsRefresh makes it); a non-interactive run such as claude -p cannot show it, so it applies the settings for that run only, records nothing, and the next interactive session shows the dialog. On v2.1.260 or later a managed CLAUDE.md delivered through the claudeMd key needs no approval, because it is instruction text rather than a command Claude Code runs — earlier versions prompt for it.

Proving the policy is in force

Everything above is a claim about other people's machines. Lesson 5.1's rule — ask for the artefact, not the verdict — applies with the stakes raised: nothing reports to you a policy that never arrived. Silence is not enforcement.

QuestionInstrumentWhat answers it
Which managed source won here?/statusSetting sources: Enterprise managed settings plus (remote), (plist), (HKLM), (file), (drop-ins), (HKCU), (parent process), (helper), or a list ending , merged
Did another source override mine?/statusSkipped sources names each managed source found and not selected (v2.1.242+)
Did the remote fetch happen?claude doctorThe Managed settings (remote) line: loaded, none configured, fetch failed, or skipped with a reason (v2.1.248+)
Which entries were dropped?claude doctorEach invalid entry with its source and field
Are the permission rules actually active for this developer?/permissionsTheir effective rules

Read the two failure shapes off the Setting sources line. The line is missing: Claude Code found no managed source delivering a policy key — check the path for that OS, and that the file contains a real policy key rather than only control keys such as managedSourcesBehavior. The line names a source you did not deploy: a higher-priority source is present and yours was ignored, and Skipped sources lists it.

When Claude Code found a managed source on the machine and didn't select it, a second line, Skipped sources, names each such source. Read it to distinguish a policy that never reached the machine from one that reached it and that a higher-priority source overrode. Requires Claude Code v2.1.242 or later. — Managed settings

That last sentence is the whole instrument, and it is the reason the table above marks the row. Take the line away and /status names the source that won or says nothing at all, and the two failures collapse into one symptom you cannot read apart: a policy that reached the machine and lost to a higher-ranked source looks exactly like a policy that never arrived — the same absent name, the same absence of any warning — and first-wins is the default that produces it.

Which is the audit's version of this level's recurring problem: the version that decides it is the one on the machine you are auditing, not the one on yours. Below v2.1.242, /status there answers the first question in the table and not the second, and the second has to be answered from the delivery side instead: go and establish which managed sources are physically present on that machine — the file, the drop-in directory, the plist, the registry value — and rank them yourself against the precedence order above. Budget for it in the review, because it is somebody walking a laptop rather than a developer reading you a line, and a review that assumed the line exists will report "the policy never reached them" about a machine where it arrived and was overridden.

For ongoing evidence rather than a one-off check, the security guide points at ConfigChange hooks to audit or block settings changes during sessions, and at OpenTelemetry for monitoring (Lesson 12.4). Know the limits before you promise an audit trail. A ConfigChange hook fires for edits to managed-settings.json and managed-settings.d/, so you can log those, but a blocking decision on a policy_settings source is ignored; and it does not run at all when server-managed settings arrive or refresh, or when a macOS managed preference or Windows registry policy changes. If MDM is your delivery mechanism, this hook sees none of your policy changes. Settings changes made in the admin console are covered separately, through the compliance API or audit log export.

What to write down

A managed policy review has four answers in it, and each one names a mechanism and how you checked. Which delivery mechanism, and why that one resists tampering on your fleet. Which controls are locks and which are defaults — availableModels and not model; sandbox network allowlists and not deny: WebFetch. What the lockdown keys break as well as prevent, allowManagedHooksOnly most of all. And the verification step, run by somebody else, on a machine you did not configure.

The fourth is the one that keeps being skipped, and it is the only one that turns the first three from a plan into a finding.

Checkpoint

  1. You deploy allowManagedHooksOnly and a marketplace allowlist through an MDM profile. A developer's /status shows Enterprise managed settings (remote). Is your policy in force, what is the second line you need, and what is the default behaviour that produced this?

    Show answer

    No. (remote) names server-managed settings as the source that won, and server-managed outranks your MDM profile, so your two keys are inert on that machine — with no warning anywhere, which is why nothing looked wrong.

    The second line is Skipped sources on /status, which names each managed source Claude Code found and did not select (v2.1.242 or later). It is the only thing that separates "reached the machine and was overridden" from "never arrived at all"; below that version you have to go and inventory which managed sources are physically present on the machine and rank them yourself. The default that produced it is managedSourcesBehavior of "first-wins": Claude Code uses the highest-ranked source that delivers at least one policy key and ignores the rest rather than merging them. Setting it to "merge" (v2.1.242 or later) applies every admin source — but only turn that on when every source below your highest one is under an administrator's control, because a lower source's list entries, hooks included, get added to the policy.

  2. A colleague's draft policy claims Claude Code "cannot reach external networks" because WebFetch is denied, and that developers are "pinned to Sonnet" because managed settings set model. Both are wrong. Correct each one with the mechanism that would actually do the job.

    Show answer

    Denying WebFetch blocks Claude's fetch tool and nothing else. If Bash is allowed, curl and wget still reach any URL. The mechanism that actually closes the gap is sandboxing, with a network domain allowlist enforced at the OS level.

    A managed model is a default, not a lock: --model and ANTHROPIC_MODEL still pick the model for that session. Deploy availableModels to restrict the choice. Both errors have the same shape — a real control over one path, written on a slide as a control over all of them, and Claude Code will never tell the author otherwise.

  3. Your team's formatter hook and status line both stop working the morning the new policy lands, and nothing in the settings file looks obviously broken. Name two distinct causes consistent with those symptoms, and the command that distinguishes them.

    Show answer

    Both symptoms are allowManagedHooksOnly doing exactly what it does: blocking user, project, local and plugin hooks, and narrowing statusLine, fileSuggestion and subagentStatusLine to managed settings — all three are command settings, so the status line travels with the hooks. Cause one is that the key is set and working as designed, and you did not exempt the team's plugin by force-enabling it in managed enabledPlugins. Cause two is that the key's value is invalid: it fails closed and is treated as true until fixed, so a typo produces identical symptoms while the file reads fine at a glance.

    claude doctor distinguishes them: it lists each invalid entry with its source and field. A dropped allowManagedHooksOnly entry named there is cause two; a clean report means the key is valid and you are looking at cause one. The invalid entry is also announced in a startup dialog on the developer's machine and on -p stderr — none of which is a place you were looking, which is why this reaches you as "everyone's hooks stopped working".

  4. Security wants to stop hooks posting to arbitrary endpoints, and engineering wants to keep its own hooks. Which key satisfies both, what does it do to your own managed policy's HTTP hooks, and why can you not describe it to security as a control a developer cannot get around?

    Show answer

    allowedHttpHookUrls. When it is defined at any settings level, Claude Code runs an HTTP hook handler only if its URL matches the merged allowlist — so engineering keeps writing hooks, and the destinations are what narrows. Because it is not managed-only, a team can even adopt it in a committed project file before an admin is involved.

    It applies to hooks from every source, managed ones included, so your own policy's HTTP hook URLs have to appear on that list or your hooks stop running too. And it is not a lock: its scope is Any file and the arrays merge across settings files, with no managed-only variant documented, so a developer can widen it by adding a pattern to ~/.claude/settings.json, and its companion httpHookAllowedEnvVars merges the same way. Describe it honestly — it stops a hook reaching an endpoint nobody chose, not a developer who wants that endpoint. If the threat model is a hostile insider, the lock is allowManagedHooksOnly, which does not merge and whose strictest value wins under "merge". Say which of the two you deployed.

Sources