Claude Code managed-settings.json: The Org-Wide Policy File Most Teams Skip

About Will

I run a multi-site content operation on Claude and Notion with autonomous agents — and I write about what we do, including what breaks.

Connect on LinkedIn →

Last week I wrote about the three-file split every team should set up in their repo: CLAUDE.md, .claude/settings.json, and .claude/settings.local.json. That gets a team to a sane shared baseline. It does not stop a single engineer with admin rights on their laptop from disabling every guardrail you wrote.

If you are deploying Claude Code to more than a handful of engineers — anyone past Series B, anyone regulated, anyone whose CISO has asked a single pointed question about AI tooling — repo-level settings are insufficient. The control you want is managed-settings.json, and most teams I talk to either do not know it exists or have not deployed it.

Where managed-settings.json Actually Lives

Claude Code reads settings in a strict precedence order. Managed settings sit at the top and cannot be overridden by anything a user does in their repo, their home directory, or their environment. The file location depends on the OS:

  • macOS: /Library/Application Support/ClaudeCode/managed-settings.json
  • Linux / WSL: /etc/claude-code/managed-settings.json
  • Windows: C:\Program Files\ClaudeCode\managed-settings.json

You push the file via whatever you already use to manage developer machines. On macOS that is MDM — Jamf, Kandji, Mosyle. On Windows it is Group Policy Preferences. On Linux fleets, your config management tool of choice — Ansible, Chef, whatever survived your last platform team rewrite. The file does not need to be created by Claude Code itself. It just needs to be present at the path above, owned and writable only by an admin account, and readable by the user running claude.

The One Rule That Earns Its Keep: permissions.deny

Of every field in managed-settings.json, the one that pays for the entire deployment effort is permissions.deny. Deny rules at the managed-settings tier take effect regardless of any allow or ask rules at lower scopes. A user cannot grant themselves permission to do something an admin has denied — not in their project settings, not in their personal settings, not via a one-time CLI flag.

Concretely, here is a minimum-viable managed file for a team that wants to stop the obvious foot-guns:

{
  "permissions": {
    "deny": [
      "Bash(curl:*)",
      "Bash(wget:*)",
      "Bash(rm -rf /*)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./**/credentials*)",
      "Read(./**/*secret*)"
    ]
  }
}

That blocks Claude from curl-ing arbitrary URLs (the most common vector for accidental data exfiltration in agentic loops), reading anything in an .env file, and deleting filesystem roots in a Bash one-liner gone wrong. It does not stop legitimate work. It stops the long tail of “I didn’t realize it would do that.”

The Drop-In Directory Is the Underrated Piece

The single-file model breaks the moment you have more than one team contributing policy. Security wants curl blocked, platform wants kubectl delete blocked, the data team wants reads against the /data/prod/ mount blocked. Funneling all three through a single admin-owned file becomes a coordination tax.

Claude Code supports a drop-in directory at managed-settings.d/ in the same parent directory as managed-settings.json. Files in that directory are merged alphabetically — same convention as systemd and sudoers.d. Layout looks like this:

/Library/Application Support/ClaudeCode/
├── managed-settings.json          # base policy
└── managed-settings.d/
    ├── 10-security.json           # security team owns
    ├── 20-platform.json           # platform team owns
    └── 30-data.json               # data team owns

Each team owns one file. They push their fragment through their own MDM channel without touching the others. Merge order is alphabetical, so the number prefix matters — later files override earlier ones for any overlapping keys, but permissions.deny rules always accumulate. Nothing a later file does can unblock something an earlier file denied.

What Belongs in Managed Settings — and What Does Not

Managed settings is a heavy hammer. Use it for things that must not be overridable. Everything else belongs in the repo’s .claude/settings.json, where engineers can iterate without filing a ticket.

Belongs in managed:

  • Deny rules for credentials, network egress, destructive shell operations
  • Telemetry / opt-out flags if your contract with Anthropic requires training data opt-out
  • Default model if you have a real reason to pin — most teams should let repos choose
  • Audit log paths if you are forwarding to a SIEM

Does not belong in managed:

  • Project-specific subagents or hooks (these live in the repo)
  • CLAUDE.md content (repo)
  • Allow rules — these are better as defaults at the repo scope, where engineers can adjust per-task

Verifying the Policy Is Actually Active

Pushing a config file is not the same as enforcing one. After deployment, run claude config list on a test machine and confirm the managed entries show up. Then attempt something the deny rule blocks — try a curl command, ask Claude to read an .env. The denial should be immediate and unambiguous, not a quiet skip. If a user can override it from their repo settings, the file is not at the right path or not readable by the user account running claude.

Model Selection at the Org Level

If you do pin a default model in managed settings — and I would argue most teams should not — read the model docs at docs.anthropic.com/en/docs/about-claude/models before writing the version string. Model identifiers change. As of this writing the workhorse is claude-sonnet-4-6, the flagship is claude-opus-4-7, and the fast option is claude-haiku-4-5-20251001. Hardcoding a model string in a managed file that nobody touches for six months is how you end up running last year’s model in production.

Where This Approach Loses

Managed settings cover the local Claude Code process. They do not cover the Anthropic Console, the Claude web app, or any MCP server an engineer connects to manually. If your threat model includes data leaving via the web app, managed settings on developer laptops are not the answer — the Enterprise plan’s org-level controls and SSO are. The two layers compose. Neither replaces the other.

Managed settings also do nothing about an engineer who runs Claude Code on a personal machine outside MDM scope. That is a device management problem, not a Claude Code problem, and the fix is the same as it has always been: do not let unmanaged machines touch production code.

The 30-Minute Rollout

  1. Pick one platform — start with whichever fleet is largest, usually macOS
  2. Write the minimum-viable managed-settings.json above
  3. Push it to one test machine via MDM, verify with claude config list
  4. Try three things the deny rules should block; confirm all three are blocked
  5. Roll to the rest of the fleet
  6. Set up the managed-settings.d/ directory so other teams can layer their own fragments without coordination

The whole exercise is half a day of work for a platform engineer who already knows your MDM. The alternative is hoping every engineer reads the same Notion page about which commands not to run. Hope is not a security control.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *