Security review for a team rollout: managed policy, allowManagedHooksOnly, allowlisted hook URLs
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 rule | What 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 rights | A 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 cache | An edit to the local cache lasts until the next successful fetch. Not forever, but not zero either |
| Other tools | Managed 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,
curlandwgetcan 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.
| Mechanism | Where | Priority | What the docs say about tampering |
|---|---|---|---|
| Server-managed | claude.ai admin console, or a self-hosted Claude apps gateway | Highest | A tampered cache applies at startup; the next successful fetch restores the policy |
| plist / registry policy | macOS com.anthropic.claudecode; Windows HKLM\SOFTWARE\Policies\ClaudeCode | High | Resists it — these require admin privileges to write |
| File-based managed | /Library/Application Support/ClaudeCode/, /etc/claude-code/, C:\Program Files\ClaudeCode\ | Medium | Nothing. The docs make the admin-privileges claim for the plist and HKLM only |
| Windows user registry | HKCU\SOFTWARE\Policies\ClaudeCode | Lowest | Writable 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 key | How it combines | Examples |
|---|---|---|
| Lists | Entries from every source | permissions.allow, hooks, sandbox.network.allowedDomains |
| Locks | The strictest value any source sets; a looser value applies only from the highest-ranked source | allowManagedHooksOnly, permissions.disableBypassPermissionsMode |
| Restriction allowlists | Taken whole from the highest source that sets it, no additions from below | availableModels, allowedMcpServers, strictKnownMarketplaces |
| Highest-ranked source only | Ignored in every lower source, even when the top one leaves it unset | apiKeyHelper, forceLoginOrgUUID, permissions.defaultMode |
| Everything else | The value from the highest source that sets it | model, 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
enabledPluginsare exempt — Hooks
The same list continues with three consequences nobody predicts from the name:
- Your
statusLine,fileSuggestionandsubagentStatusLinesettings are narrowed to managed settings — all three arecommandsettings, so they travel with the hooks. - Plugins with a
commandsource are disabled, including ones force-enabled in managedenabledPlugins, unlessdisableCommandPluginSourcesis explicitlyfalse. Command sources need v2.1.229 or later to exist at all. - Marketplace
headersHelpercommands 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.
{
"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.
| Question | Instrument | What answers it |
|---|---|---|
| Which managed source won here? | /status | Setting 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? | /status | Skipped sources names each managed source found and not selected (v2.1.242+) |
| Did the remote fetch happen? | claude doctor | The Managed settings (remote) line: loaded, none configured, fetch failed, or skipped with a reason (v2.1.248+) |
| Which entries were dropped? | claude doctor | Each invalid entry with its source and field |
| Are the permission rules actually active for this developer? | /permissions | Their 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
-
You deploy
allowManagedHooksOnlyand a marketplace allowlist through an MDM profile. A developer's/statusshowsEnterprise 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 answerAnswer
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 sourceson/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 ismanagedSourcesBehaviorof"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. -
A colleague's draft policy claims Claude Code "cannot reach external networks" because
WebFetchis denied, and that developers are "pinned to Sonnet" because managed settings setmodel. Both are wrong. Correct each one with the mechanism that would actually do the job.Show answerAnswer
Denying
WebFetchblocks Claude's fetch tool and nothing else. If Bash is allowed,curlandwgetstill reach any URL. The mechanism that actually closes the gap is sandboxing, with a network domain allowlist enforced at the OS level.A managed
modelis a default, not a lock:--modelandANTHROPIC_MODELstill pick the model for that session. DeployavailableModelsto 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. -
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 answerAnswer
Both symptoms are
allowManagedHooksOnlydoing exactly what it does: blocking user, project, local and plugin hooks, and narrowingstatusLine,fileSuggestionandsubagentStatusLineto managed settings — all three arecommandsettings, 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 managedenabledPlugins. Cause two is that the key's value is invalid: it fails closed and is treated astrueuntil fixed, so a typo produces identical symptoms while the file reads fine at a glance.claude doctordistinguishes them: it lists each invalid entry with its source and field. A droppedallowManagedHooksOnlyentry 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-pstderr — none of which is a place you were looking, which is why this reaches you as "everyone's hooks stopped working". -
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 answerAnswer
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 fileand 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 companionhttpHookAllowedEnvVarsmerges 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 isallowManagedHooksOnly, which does not merge and whose strictest value wins under"merge". Say which of the two you deployed.
Sources
- code.claude.com/docs/en/admin-setup
- code.claude.com/docs/en/managed-settings
- code.claude.com/docs/en/server-managed-settings
- code.claude.com/docs/en/hooks
- code.claude.com/docs/en/plugin-marketplaces
- code.claude.com/docs/en/security
- code.claude.com/docs/en/settings
- code.claude.com/docs/en/settings-reference