[BUG] Empty server-managed settings (304 cached) zero out local managed-settings.json — deny/allow rules never enforced

Status Open
Maintainer reply None cached
Activity 5 comments · opened Jun 23, 2026

Platform: Linux (RHEL 9)
Claude Code version: current (tested 2026-06-22)

Problem

When a facility deploys /etc/claude-code/managed-settings.json with permissions.allow and permissions.deny rules, those rules are silently discarded at startup. All policySettings destinations end up with 0 rules regardless of what the file contains.

Root cause (from debug log)

The documented precedence is server-managed > managed-settings.json. When the deployment has no server-managed policy configured with Anthropic, the server returns an empty/cached 304 response. Claude Code treats this as "server-managed policy = zero rules" and replaces policySettings with empty arrays, overwriting the local file.

\\\
[DEBUG] MDM settings load completed in 2ms # local file read
[DEBUG] Remote managed settings loaded # empty 304 from Anthropic
[DEBUG] Replacing all allow rules for destination 'policySettings' with 0 rule(s): []
[DEBUG] Replacing all deny rules for destination 'policySettings' with 0 rule(s): []
\
\\

The local file is valid and readable (managed-settings.json is world-readable, no symlink issues).

Expected behavior

An empty/absent server-managed response should be treated as "not configured" and fall through to managed-settings.json. It should not be treated as a configured policy of zero rules that overrides the local file.

Actual behavior

All policySettings allow/deny rules are zeroed out. Any rules in managed-settings.json — including deny rules meant to restrict tool access — are never enforced.

Impact

Facility-wide deny rules (e.g., Bash(condor_*)) are never enforced on shared systems. Users can run any command without restriction, defeating the purpose of managed settings for shared multi-user environments.

Reproduction

  1. Deploy /etc/claude-code/managed-settings.json with deny rules on a Linux system with no Anthropic enterprise/server-managed policy configured
  2. Run claude --debug-file /tmp/debug.log -p hello
  3. Grep for policySettings in the debug log — all rules show 0 rule(s): [] despite the valid local file

Example managed-settings.json

{
  "permissions": {
    "allow": ["Bash(pixi*)", "Bash(du*)", "Bash(df*)"],
    "deny": ["Bash(condor_*)"]
  }
}

Notes

  • The same behavior occurs with managed-settings.d/ drop-in files (tested prior to switching to managed-settings.json)
  • The debug log confirms the file is being watched: Watching for changes in setting files ... /etc/claude-code/managed-settings.json... and drop-in directory /etc/claude-code/managed-settings.d
  • The remote settings fetch returns 304 (cached), suggesting this affects any deployment without a paid Anthropic enterprise MDM policy

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/53662
  2. https://github.com/anthropics/claude-code/issues/56671
  3. https://github.com/anthropics/claude-code/issues/44640

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

yurukusa · 2 months ago

This is a nasty one because it's a silent failure of a deny rule — the operator believes a facility-wide restriction is in force, and it simply isn't. On a shared multi-user box that's a real security gap, not just a config annoyance, so thank you for the precise root-cause and the debug trace. The "empty/304 server-managed response should mean not configured, fall through to local" expectation looks right to me, and Replacing all deny rules ... with 0 rule(s): [] overwriting a valid local file is the bug.
While that's triaged, here's a mitigation that doesn't depend on the broken policySettings path at all, so your Bash(condor_*) deny can be enforced again today.
A PreToolUse hook runs before the tool executes and is evaluated independently of the allow/deny permission merge. So even when policySettings has been zeroed, a hook can hard-block the command deterministically:

cmd="$(jq -r '.tool_input.command // ""')"
if printf '%s' "$cmd" | grep -Eq '(^|[^[:alnum:]_])condor_'; then
  echo "Blocked by facility policy: condor_* is not permitted on this system." >&2
  exit 2   # exit 2 = block the tool call and surface the message
fi
exit 0

Wired in managed-settings.json:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [
        { "type": "command", "command": "/etc/claude-code/hooks/deny-enforce.sh" }
      ]}
    ]
  }
}

Two honest caveats, because I don't want to hand you a false sense of safety:

  1. I haven't verified on your exact build whether the same 304-zeroing also affects the hooks destination (your trace only shows policySettings). Please run your claude --debug-file repro and grep for the hook loading / PreToolUse lines to confirm the hook is actually registered. If hooks survives the merge, the snippet above closes the gap; if it gets zeroed too, that's a second instance of the same root cause and worth noting in this issue.
  2. For a genuinely multi-user threat model, anything inside Claude Code can in principle be worked around by a determined user. The only fully robust place to enforce "no condor_*" is the OS layer (the scheduler's own auth, a wrapper, or sudo/PAM rules). Treat the hook as defense-in-depth, not the sole control, on shared systems.

If it helps, the free cc-safe-setup repo has a permission-denial-enforcer hook that implements exactly this independent-enforcement pattern (and the pinning/precedence edge cases around it) — reference implementation, nothing to buy: https://github.com/yurukusa/cc-safe-setup
To be clear on what's first-hand vs not: the 304/precedence root cause is your analysis (which reads correctly to me); the PreToolUse-runs-regardless-of-policySettings behavior and the exit 2 block are things I've used and can vouch for. The caveat about whether hooks is also zeroed is exactly the part I haven't tested on your build.

kristoffersingleton-yale · 1 month ago

Possibly the same root-cause family: #72634 shows the policyHelper rung of the same documented managed-tier ladder also losing to the remote payload — in that case the helper is documented to preempt server-managed settings entirely, yet on macOS/v2.1.205 it executes and its well-formed output is silently discarded (canary-verified reproduction in that issue's comments). Both behaviors are consistent with the remote cache unconditionally winning whenever it exists, regardless of the documented precedence.

Wopple · 1 month ago

I was hoping to use managed settings as a way to enforce tool constraints in a sandbox environment, so this bug is problematic for me as well. My use case is I want to prevent claude from making all web requests except those that are explicitly allowed assuming an adversarial claude (either through misalignment or through prompt injection). Simply setting up a proxy that it must connect to isn't enough, because the web search and fetch tools are run on Anthropic's side, not in my process. Since I must have the Anthropic API exposed for claude to work, it can make any web request it wants even if I set up a local proxy. I _was_ planning to use managed permissions to prevent those tools from being run, but it seems they are ignored. I do not know if --dangerously-skip-permissions is relevant here, but I do want to run with that flag. The sandboxed environment that claude runs in is what will make it safe, but I don't want it asking the user for approvals. I can easily configure the managed settings to enforce immutability, but the same is not true of the user settings which lives in claude's user directory which it needs to edit for benign use cases like authentication.

Showing cached comments. Read the full discussion on GitHub ↗