[BUG] managed-settings.d hooks are silently dropped when the signed-in org has server-managed settings — they only load when the remote fetch 404s

Status Open
Reported on v2.1.87
Maintainer reply None cached
Activity 0 comments · opened Aug 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

A hook deployed through the managed-settings.d/ drop-in directory is registered and enforced only when the signed-in organization has no server-managed settings — i.e. when the remote settings fetch returns 404.

As soon as I sign into an organization that has any server-managed settings saved in the admin console (fetch returns 304), the drop-in hooks are never registered and the hook command is never executed. There is no warning and no error; nothing in the debug log names the file that was dropped.

/status shows the substitution directly. Same machine, same binary, same drop-in file on disk:

  • org without server-managed settings → Setting sources: ... Enterprise managed settings (drop-ins)
  • org with server-managed settings → Setting sources: ... Enterprise managed settings (remote)

The managed tier appears to hold a single source slot. When remote settings are fetched successfully they take that slot, and the drop-in directory is not listed at all.

The remote payload in my case is a single unrelated key — it contains no hooks key and no permissions key — yet it still displaces the entire local drop-in source.

I verified the local file is never touched: a 2-second poll on its SHA-256 plus an EndpointSecurity watch (create/unlink/rename/truncate/setmode/setowner) across the whole experiment. The hash never changed and there were no delete or permission events. The file is simply not loaded.

This matters because managed-settings.d hooks are how an organisation deploys PreToolUse gating and audit logging from MDM. When this happens, the enforcement hook stops running and its audit logging stops at the same moment, so the deactivation is not recorded anywhere. Meanwhile the file is still on disk and unmodified, so MDM inventory keeps reporting the policy as deployed. An end user can toggle all of this by signing into a different organization.

What Should Happen?

Hooks deployed through managed-settings.d/ should be honoured regardless of whether the signed-in organization also has server-managed settings.

The docs define deep-merge semantics within the managed tier:

"Following the systemd convention, Claude Code merges managed-settings.json first as the base, then sorts all *.json files in the drop-in directory alphabetically and merges them on top. For scalar values, Claude Code lets later files override earlier ones; it concatenates and de-duplicates arrays and deep-merges objects."

but they do not define precedence between the four managed delivery mechanisms (server-managed, MDM/OS policy, managed-settings.json, managed-settings.d/). Remote presence wholesale suppressing the local drop-in is undocumented and inconsistent with the merge semantics documented one level down.

Expected: the sources merge, so a remote payload carrying an unrelated key does not remove locally deployed hooks.

At an absolute minimum, silently discarding a locally deployed managed policy should emit a warning, and /status should list every managed source in effect rather than only one.

Error Messages/Logs

# org-A — no server-managed settings — drop-in hook RUNS
[DEBUG] Remote settings: No settings found (404)
[DEBUG] Remote settings: Saved to /Users/<user>/.claude/remote-settings.json
[DEBUG] Remote settings: Saved empty sentinel (404 response)

/status → Setting sources: User settings, Shared project settings,
                           Project local settings,
                           Enterprise managed settings (drop-ins)

$ cat /tmp/managed-hook.log
15:03:12 drop-in hook fired


# org-B — has server-managed settings — drop-in hook DOES NOT RUN
[DEBUG] Remote settings: Using cached settings (304)
[DEBUG] Remote settings: Cache still valid (304 Not Modified)

/status → Setting sources: User settings, Shared project settings,
                           Project local settings,
                           Enterprise managed settings (remote)

$ cat /tmp/managed-hook.log
cat: /tmp/managed-hook.log: No such file or directory


# Both runs contain these lines, so the drop-in directory IS discovered and
# watched in both cases. Nothing reports it being rejected or overridden:
[DEBUG] Watching for changes in setting files /Users/<user>/.claude/settings.json,
        /Library/Application Support/ClaudeCode/managed-settings.json...
        and drop-in directory /Library/Application Support/ClaudeCode/managed-settings.d
[DEBUG] Programmatic settings change notification for policySettings
[DEBUG] Settings changed from policySettings, updating app state

# The only hook registration line present in either run is for plugins:
[DEBUG] Registered 4 hooks from 4 plugins

Steps to Reproduce

Requires two organizations on the same account: org-A with no server-managed settings in the admin console, org-B with at least one server-managed setting saved.

  1. Create the drop-in hook at

/Library/Application Support/ClaudeCode/managed-settings.d/00-test.json:

``json
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "date '+%H:%M:%S drop-in hook fired' >> /tmp/managed-hook.log" }
]
}
]
}
}
``

  1. rm -f /tmp/managed-hook.log
  1. Sign in to org-A (no server-managed settings). Restart Claude Code, then run:

``
claude --debug-file /tmp/a.log -p ok </dev/null
``

  • /tmp/managed-hook.log → gains a line ✅
  • /tmp/a.logRemote settings: No settings found (404)
  • /statusEnterprise managed settings (drop-ins)
  • ~/.claude/remote-settings.json{}
  1. rm -f /tmp/managed-hook.log
  1. Sign in to org-B (has server-managed settings). Restart Claude Code, then run:

``
claude --debug-file /tmp/b.log -p ok </dev/null
``

  • /tmp/managed-hook.logis never created
  • /tmp/b.logRemote settings: Using cached settings (304)
  • /statusEnterprise managed settings (remote)
  • ~/.claude/remote-settings.json → the org's payload (mine: one unrelated key, no hooks, no permissions)

The drop-in file is byte-identical to step 3 — only the signed-in organization changed.

Note — restarting the process is required. Running /login alone does not re-evaluate this; an already-running process keeps whatever it loaded at startup. In my first attempts this masked the behaviour entirely.

Control: it is the API-fetch path, not the payload content

CLAUDE_CODE_REMOTE_SETTINGS_PATH makes the client load remote settings from a local file and skip the API fetch. Feeding it the exact same JSON org-B had cached does not reproduce the bug:

| CLAUDE_CODE_REMOTE_SETTINGS_PATH content | drop-in hook runs |
|---|---|
| {} | yes |
| org-B's exact cached payload | yes |
| {} (repeat) | yes |

So the trigger is neither the content of the remote settings nor merely their presence — it is specifically the code path taken when remote managed settings are successfully fetched from the API.

Version matrix — not a regression

I re-ran the same A/B across ten versions spanning ~4.5 months. Same machine, same drop-in file, only the signed-in org changing:

| Version | Released | org-A (404) | org-B (304) |
|---|---|---|---|
| 2.1.87 | 2026-03-29 | hook runs | hook does not run |
| 2.1.100 | 2026-04 | hook runs | hook does not run |
| 2.1.150 | 2026-05 | hook runs | hook does not run |
| 2.1.191 | 2026-06-24 | hook runs | hook does not run |
| 2.1.220 | 2026-07-24 | hook runs | hook does not run |
| 2.1.223 | 2026-08-05 | hook runs | hook does not run |
| 2.1.226 | 2026-08-08 | hook runs | hook does not run |
| 2.1.227 | 2026-08-10 | hook runs | hook does not run |
| 2.1.228 | 2026-08-11 | hook runs | hook does not run |
| 2.1.229 | 2026-08-12 | hook runs | hook does not run |

Completely consistent, so this is long-standing rather than a recent regression. 2.1.87 is the first release after the managed-settings.d drop-in directory was documented (#38555, 2026-03-28), which suggests the behaviour has been present since the feature shipped.

The org-A column also confirms every one of these versions loads managed-settings.d correctly — drop-in support itself is not the problem.

Claude Model

Not sure / Multiple models

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.229 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Environment: macOS 26.5.2 (Darwin 25.5.0), arm64, Claude Code 2.1.229, claude.ai OAuth on a Team plan. The drop-in file is deployed by an MDM agent; the minimal 00-test.json above reproduces it standalone with no MDM involved.

Related issues — same class, but each differs from this one:

  • #70181 — empty server-managed settings (304) zero out local managed-settings.json permission rules. Scoped to permissions, on Linux. This report is about hooks, via the managed-settings.d drop-in, on macOS, and triggers on a non-empty remote payload that merely lacks the relevant key.
  • #72634 — policyHelper in local managed-settings.json ignored when org server-managed settings are present (Linux).
  • #75149 — feature request to merge managed settings sources instead of having one win. That request describes the fix this bug needs.
  • #38555 — docs for the managed-settings.d/ drop-in directory.

One caveat worth flagging. The log line #70181 cites —

[DEBUG] Replacing all deny rules for destination 'policySettings' with 0 rule(s): []

— appears in both my 404 and 304 runs, so that line alone is not the discriminator for the hook case. This may be a related but distinct code path rather than the same defect, and I did not want to assert they are identical.

View original on GitHub ↗