A practical security-posture reference for organisations deploying Claude Code across staff or contractor laptops. What the platform exposes, what an enterprise can actually control, what it can't, and a worked managed-settings.json you can adapt.
Claude Code is a developer agent with substantial capability on the user's machine. By default it can:
~/.claude/projects/... indefinitely, and optionally upload them to claude.ai.The threat model for an enterprise deploying it is the same as for any privileged developer tool, plus the new AI-specific concerns of prompt-borne data exfiltration and supply-chain risk via the plugin/MCP ecosystem.
| Risk | Concrete example |
|---|---|
| Prompt-borne data exfiltrationHigh | Developer pastes customer PII or source code into a prompt; it goes to Anthropic and (optionally) any wired-up MCP server. |
| Unauthorised tool executionHigh | Agent runs rm -rf, curl | sh, git reset --hard or a destructive migration without the user reading the permission prompt. |
| Permission-prompt bypassHigh | User passes --dangerously-skip-permissions or has it baked into a wrapper script. |
| Plugin / marketplace supply chainMedium | User installs a community plugin or marketplace that ships a malicious skill, hook, or MCP server. |
| MCP server abuseMedium | User wires up an MCP server that reads/writes far more than the immediate task needs (e.g. full Gmail mailbox). |
| Hook abuseMedium | A user-installed hook silently posts prompt content to an attacker-controlled URL. |
| Cross-account confusionMedium | User signs in with a personal Claude account instead of the corporate one — usage and data leave the corporate boundary. |
| Session data on diskCompliance | Months of conversation transcripts (with embedded secrets) sit unencrypted in the user's home directory; or get auto-uploaded to claude.ai. |
| Stale-version compliance gapCompliance | Old Claude Code versions miss security fixes; the auto-updater pulls a release the security team hasn't validated. |
The rest of this document maps each of these to the specific settings, hooks, and deployment patterns that mitigate them.
Claude Code resolves settings in strict order — managed (highest priority) → project policy → user → project → local. Higher tiers can both override values and lock down whether lower tiers may set them at all.
| Tier | Path | Writable by |
|---|---|---|
| Managed (enterprise) |
macOS /Library/Application Support/ClaudeCode/managed-settings.jsonLinux/WSL /etc/claude-code/managed-settings.jsonWindows C:\Program Files\ClaudeCode\managed-settings.json
|
Admin / MDM |
| Managed drop-ins | .../managed-settings.d/*.json — fragments merge alphabetically |
Admin / MDM |
| User | ~/.claude/settings.json |
The user |
| Project (shared) | .claude/settings.json in the repo |
Anyone who can commit |
| Project (local) | .claude/settings.local.json (gitignored) |
The user |
Windows can additionally drive managed settings from HKLM\SOFTWARE\Policies\ClaudeCode (Group Policy). WSL can be made to inherit Windows policy by setting wslInheritsWindowsSettings: true in both an admin-only Windows source and HKCU (double opt-in).
Several settings are policy-only: they're respected only when set in managed settings, and they constrain everything below them. These are the load-bearing controls for an enterprise deployment:
allowManagedHooksOnly — ignore user/project hooks entirelyallowManagedPermissionRulesOnly — ignore user/project allow/deny rulesallowManagedMcpServersOnly — ignore user/project MCP allowlists (denylist still merges)strictPluginOnlyCustomization — block non-plugin customisation surfacesstrictKnownMarketplaces — strict allowlist of marketplacesblockedMarketplaces — strict denylist of marketplacesforceRemoteSettingsRefresh — block startup until managed settings are freshly fetchedforceLoginMethod / forceLoginOrgUUID — enforce which account logs inavailableModels / modelOverrides — restrict which models can be selectedpluginTrustMessage — custom legalese on the plugin trust promptdisableSkillShellExecution — kill inline shell execution in non-managed skillsIf you only remember one thing: managed settings + MDM is the layer that makes the rest of this document possible. Without it you can recommend, but you cannot enforce.
Each subsection below lists the settings and patterns that address one of the threats from §1. Lift the relevant snippets into your managed-settings.json.
The single highest-value control is a UserPromptSubmit hook that runs DLP on every prompt before it leaves the user's machine. The hook fires synchronously, has the prompt text on stdin, and can block submission by exiting with code 2.
{
"allowManagedHooksOnly": true,
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "/usr/local/bin/cc-dlp-check",
"timeout": 5
}
]
}
]
}
}
A minimal cc-dlp-check reads the prompt JSON from stdin, scans for company PII patterns (customer IDs, source-of-record IDs, NHS numbers, source-code markers, AWS keys, etc.) and exits non-zero with a clear message if it finds any. Tune patterns to match the org's data classes — generic DLP regex packs miss most useful signals.
The same pattern works on PreToolUse for Bash, Edit, Write, WebFetch, and on each MCP server — so an MCP tool call carrying sensitive data can be blocked before it leaves the box.
Claude Code's bundled sandbox supports per-domain network policy. Keep the allowlist tight — Anthropic's API, your code host, your registry, and nothing else — so that even a compromised hook or MCP server can't reach an attacker-controlled endpoint.
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"network": {
"allowedDomains": [
"api.anthropic.com",
"*.api.anthropic.com",
"github.com",
"*.github.com",
"registry.npmjs.org",
"pypi.org",
"*.pythonhosted.org"
],
"allowManagedDomainsOnly": true,
"deniedDomains": [
"*.pastebin.com",
"*.ngrok.io",
"*.serveo.net",
"transfer.sh"
]
}
}
}
allowManagedDomainsOnly: true is the lockdown — without it, users or plugins could append to the allowlist from a lower-priority settings file. deniedDomains merges from all sources regardless, so an end user can block more for themselves.
Local transcripts live at ~/.claude/projects/<sanitised-cwd>/<session>.jsonl and contain the full conversation, including any sensitive content typed in. Default retention is 30 days.
{
"cleanupPeriodDays": 7,
"autoUploadSessions": false,
"agentPushNotifEnabled": false,
"inputNeededNotifEnabled": false,
"channelsEnabled": false
}
cleanupPeriodDays — lower the retention window. Minimum 1.autoUploadSessions: false — prevents mirroring local sessions to claude.ai as view-only. Default is already false; be explicit.agentPushNotifEnabled / inputNeededNotifEnabled — disable proactive mobile push notifications that can carry prompt/output content off the device.channelsEnabled: false — disable inbound channel messages from MCP servers with the claude/channel capability.For a hard "no transcripts on disk", users can launch with claude --no-session-persistence. There is no global setting to enforce this — consider wrapping the binary or a SessionStart hook that exits non-zero if persistence is enabled.
The default permission system is good — by design it asks before every destructive operation. Two real-world attack paths defeat it:
--dangerously-skip-permissions (or accepts the bypass dialog once and forgets).Bash(*)) is too broad.Lock both down in managed settings:
{
"permissions": {
"defaultMode": "default",
"disableBypassPermissionsMode": "disable",
"disableAutoMode": "disable",
"deny": [
"Bash(rm -rf *)",
"Bash(sudo *)",
"Bash(curl * | sh)",
"Bash(curl * | bash)",
"Bash(wget * | sh)",
"Bash(git reset --hard *)",
"Bash(git push --force*)",
"Bash(git push -f *)",
"Bash(docker run *--privileged*)",
"Bash(chmod 777 *)",
"WebFetch(domain:pastebin.com)",
"WebFetch(domain:ngrok.io)",
"Read(/etc/shadow)",
"Read(~/.ssh/**)",
"Read(~/.aws/credentials)",
"Read(~/.gnupg/**)"
],
"allow": [
"Read(./**)",
"Edit(./**)"
]
},
"allowManagedPermissionRulesOnly": true,
"skipDangerousModePermissionPrompt": false,
"skipAutoPermissionPrompt": false
}
disableBypassPermissionsMode: "disable" makes --dangerously-skip-permissions a no-op. The user can still type it; nothing happens.disableAutoMode: "disable" similarly removes auto mode from the menu.allowManagedPermissionRulesOnly ignores any allow/deny rules at lower tiers — so a user's overly-broad Bash(*) is silently ignored.deny is denylist-takes-precedence: if a rule appears in both allow and deny, deny wins, including against managed allow rules.~/.ssh, AWS, GPG, GCloud, kube configs, browser profiles, etc. blunt the "Claude asked nicely and the user accepted" prompt attacks.For high-security deployments, run the platform sandbox so that even an approved tool call can't reach outside its lane:
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"autoAllowBashIfSandboxed": true,
"filesystem": {
"denyRead": [
"/Users/*/Library/Application Support/Google/Chrome",
"/Users/*/Library/Application Support/Slack",
"/Users/*/.ssh",
"/Users/*/.aws",
"/Users/*/.gnupg",
"/Users/*/.kube"
],
"denyWrite": [
"/etc/**",
"/Library/LaunchDaemons/**",
"/Users/*/Library/LaunchAgents/**"
]
}
}
}
allowUnsandboxedCommands: false makes the dangerouslyDisableSandbox Bash parameter a no-op.failIfUnavailable: true exits at startup if the sandbox can't start. Use this when sandboxing is a hard gate; default is "warn and continue unsandboxed", which silently weakens the posture.Plugins, skills, agents, hooks, MCP servers and marketplaces are all arbitrary-code execution vectors. The default install lets users add any of them at will. For enterprise:
{
"strictPluginOnlyCustomization": ["skills", "agents", "hooks", "mcp"],
"strictKnownMarketplaces": [
{ "source": "github", "repo": "your-org/claude-marketplace" },
{ "source": "hostPattern", "hostPattern": "^github\\.your-org\\.com$" }
],
"extraKnownMarketplaces": {
"internal": {
"source": { "source": "github", "repo": "your-org/claude-marketplace" }
}
},
"allowedMcpServers": [
{ "serverName": "internal-jira", "serverUrl": "https://mcp.your-org.com/*" },
{ "serverName": "github-readonly", "serverCommand": ["npx", "-y", "@modelcontextprotocol/server-github"] }
],
"deniedMcpServers": [
{ "serverName": "filesystem-unrestricted" }
],
"allowManagedMcpServersOnly": true,
"disableSkillShellExecution": true,
"pluginTrustMessage": "Plugins are vetted by IT Security before approval. Installing a plugin not from your-org/claude-marketplace is a violation of company AI Usage Policy."
}
strictPluginOnlyCustomization is the big hammer: blocks ~/.claude/skills/, project .claude/skills/, user-defined hooks in settings.json, and .mcp.json for the surfaces listed. Approved-marketplace plugins still work.strictKnownMarketplaces is enforced before download — blocked sources never touch the filesystem.allowedMcpServers lets you allowlist by name+URL pattern, exact stdio command, or both. allowManagedMcpServersOnly: true then ignores all user/project allowlists.disableSkillShellExecution: true disables the !command inline shell syntax inside user/project skills and custom slash commands. Plugin-shipped skills are unaffected.
Vetting bar. An MCP server should clear the same bar as a new SaaS vendor: data-flow review, OAuth scope review, DPIA where needed. "Just install it locally" is a vendor-onboarding bypass and worth flagging in the governance review process from SERVICE.md Phase 2.
{
"forceLoginMethod": "claudeai",
"forceLoginOrgUUID": [
"11111111-2222-3333-4444-555555555555"
],
"availableModels": [
"opus-4-7",
"sonnet-4-6",
"haiku-4-5"
]
}
forceLoginMethod — forces Claude.ai (for Pro/Teams/Enterprise) or Console (for API billing). Stops users on a corporate Teams plan from accidentally signing in with a personal API key.forceLoginOrgUUID — hard-pin the org. Login fails if the authenticated account doesn't belong to one of the listed organisations.availableModels — if compliance requires "Anthropic models only, no Bedrock fallback", or "Sonnet/Haiku only, no Opus burn rate", this is the gate.For Bedrock/Vertex/Foundry-routed deployments, set the relevant environment variables in the managed env block so users can't accidentally bypass the gateway.
You can't respond to what you can't see. A baseline managed audit pipeline:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "https://siem.your-org.com/claude-code/bash",
"timeout": 3,
"headers": { "Authorization": "Bearer $SIEM_TOKEN" },
"allowedEnvVars": ["SIEM_TOKEN"]
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "http",
"url": "https://siem.your-org.com/claude-code/prompt",
"timeout": 3,
"async": true
}
]
}
]
},
"allowedHttpHookUrls": ["https://siem.your-org.com/*"],
"httpHookAllowedEnvVars": ["SIEM_TOKEN"]
}
allowedHttpHookUrls is a managed-only allowlist of where HTTP hooks may target. Without it, a malicious hook could post anywhere.httpHookAllowedEnvVars restricts which env vars HTTP hooks may interpolate into headers — closes the env-var exfiltration vector.async: true on the prompt hook means submission isn't blocked on the SIEM round-trip. Keep DLP checks synchronous; keep audit logging async.{
"minimumVersion": "2.1.75",
"autoUpdatesChannel": "stable",
"env": { "DISABLE_AUTOUPDATER": "0" }
}
minimumVersion prevents downgrade attacks where a user pins an older binary.autoUpdatesChannel: "stable" (not latest or rc) keeps users on the conservative track.Hooks are arbitrary code execution. They're the most powerful control you have and the most powerful vector if mishandled.
{
"allowManagedHooksOnly": true,
"allowedHttpHookUrls": ["https://siem.your-org.com/*"],
"httpHookAllowedEnvVars": ["SIEM_TOKEN"]
}
allowManagedHooksOnly: true ignores all user/project hooks. Only managed-settings hooks run. Appropriate for hardened deployments — you lose user-side conveniences (linter on save), so set expectations or whitelist those via plugins.disableAllHooks: true disables hooks entirely, including the managed ones — almost never what you want.managed-settings.jsonThe example below combines the snippets above into a single deployable file. Adapt the marketplace/MCP names, SIEM URL, login org UUID and denylists for your org. Treat it as code: version-control it, code-review changes, roll it out via your normal MDM change process.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"forceLoginMethod": "claudeai",
"forceLoginOrgUUID": ["YOUR-ORG-UUID-HERE"],
"forceRemoteSettingsRefresh": true,
"availableModels": ["opus-4-7", "sonnet-4-6", "haiku-4-5"],
"minimumVersion": "2.1.75",
"autoUpdatesChannel": "stable",
"cleanupPeriodDays": 7,
"autoUploadSessions": false,
"agentPushNotifEnabled": false,
"inputNeededNotifEnabled": false,
"channelsEnabled": false,
"permissions": {
"defaultMode": "default",
"disableBypassPermissionsMode": "disable",
"disableAutoMode": "disable",
"deny": [
"Bash(rm -rf *)", "Bash(sudo *)",
"Bash(curl * | sh)", "Bash(curl * | bash)",
"Bash(wget * | sh)",
"Bash(git reset --hard *)",
"Bash(git push --force*)", "Bash(git push -f *)",
"Bash(docker run *--privileged*)",
"Read(~/.ssh/**)", "Read(~/.aws/credentials)",
"Read(~/.aws/config)", "Read(~/.gnupg/**)",
"Read(~/.kube/**)", "Read(/etc/shadow)",
"WebFetch(domain:pastebin.com)",
"WebFetch(domain:ngrok.io)",
"WebFetch(domain:transfer.sh)"
]
},
"allowManagedPermissionRulesOnly": true,
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": [
"api.anthropic.com", "*.api.anthropic.com",
"github.com", "*.github.com",
"registry.npmjs.org",
"pypi.org", "*.pythonhosted.org"
],
"allowManagedDomainsOnly": true,
"deniedDomains": [
"*.pastebin.com", "*.ngrok.io",
"*.serveo.net", "transfer.sh"
]
},
"filesystem": {
"denyRead": [
"/Users/*/.ssh", "/Users/*/.aws",
"/Users/*/.gnupg", "/Users/*/.kube"
]
}
},
"strictPluginOnlyCustomization": ["skills", "agents", "hooks", "mcp"],
"strictKnownMarketplaces": [
{ "source": "github", "repo": "your-org/claude-marketplace" }
],
"extraKnownMarketplaces": {
"internal": {
"source": { "source": "github", "repo": "your-org/claude-marketplace" }
}
},
"allowedMcpServers": [
{ "serverName": "internal-jira", "serverUrl": "https://mcp.your-org.com/*" }
],
"allowManagedMcpServersOnly": true,
"deniedMcpServers": [
{ "serverName": "filesystem-unrestricted" }
],
"disableSkillShellExecution": true,
"pluginTrustMessage": "Plugins must come from your-org/claude-marketplace. Installing third-party plugins is a violation of company AI Usage Policy.",
"hooks": {
"UserPromptSubmit": [{
"hooks": [
{ "type": "command", "command": "/usr/local/bin/cc-dlp-check", "timeout": 5 },
{ "type": "http", "url": "https://siem.your-org.com/claude-code/prompt", "async": true, "timeout": 3 }
]
}],
"PreToolUse": [{
"matcher": "Bash",
"hooks": [
{ "type": "http", "url": "https://siem.your-org.com/claude-code/bash", "async": true, "timeout": 3 }
]
}]
},
"allowManagedHooksOnly": true,
"allowedHttpHookUrls": ["https://siem.your-org.com/*"],
"httpHookAllowedEnvVars": ["SIEM_TOKEN"]
}
The managed-settings file is just a JSON file on disk at a known path. MDM deployment is straightforward — what matters is filesystem ACLs, so the user can't overwrite it.
/Library/Application Support/ClaudeCode/managed-settings.json with chown root:wheel and chmod 644.managed-settings.d/ alongside it to layer policy fragments (one file from security, one from platform — they merge alphabetically).cc-dlp-check to /usr/local/bin/ with chmod 755 so the DLP hook works everywhere./etc/claude-code/managed-settings.json as root, mode 644./etc/claude-code/managed-settings.d/ for layered fragments.wslInheritsWindowsSettings: true in both the Windows HKLM/Program Files source and the HKCU registry — double opt-in is required.managed-settings.json at C:\Program Files\ClaudeCode\. The legacy C:\ProgramData\ClaudeCode\ path was removed in v2.1.75.HKLM\SOFTWARE\Policies\ClaudeCode — same schema, registry form.Users can run /status and the managed-settings-loaded indicator shows in the diagnostic output. From an admin's perspective, forceRemoteSettingsRefresh: true plus a startup-blocking check is the surest way to confirm policy is being applied — the binary refuses to start without it.
Be honest with stakeholders about the gaps. None of these are showstoppers, but they shape the threat model.
~/bin/claude won't read managed settings located at admin paths. On managed devices, restrict execution to the approved install location via Gatekeeper / AppLocker / Bouncer.managed-settings.json. Two separate controls programmes.strictKnownMarketplaces controls the source, not the trustworthiness of the code inside. Treat the internal marketplace like an internal NPM registry — vet, sign, version.apiKeyHelper is a script. If a malicious plugin can write to its path, it can replace the credential provider. The managed file is read-only for users, but the helper it references may not be — pin the path under /usr/local/bin/ and protect with ACLs.cleanupPeriodDays reduces the window but doesn't eliminate it. For genuinely high-sensitivity environments, run Claude Code in a containerised or ephemeral environment whose disk is wiped on logout.A short version for an engagement readout, or as a Q-set for the AI security posture quiz. Hover to tick.
forceLoginMethod + forceLoginOrgUUID set to the corporate accountavailableModels restricts to approved modelsminimumVersion set to a security-validated versionallowManagedPermissionRulesOnly, disableBypassPermissionsMode, explicit deny rules for destructive commands & sensitive pathsfailIfUnavailable: true and allowUnsandboxedCommands: falseallowManagedDomainsOnly: true, allowlist scoped to Anthropic + code host + registriesstrictPluginOnlyCustomization covering all four surfaces; strictKnownMarketplaces pointing at the internal marketplaceallowManagedMcpServersOnly: true with a named server allowlistUserPromptSubmit hook deployed under /usr/local/bin/ with patterns tuned to org data classesallowedHttpHookUrls locked to the SIEM domaincleanupPeriodDays), autoUploadSessions: falseThis document covers controls inside the Claude Code product. The full defensive picture includes:
claude.ai, api.anthropic.com, chat.openai.com, gemini.google.com, copilot.microsoft.com etc. — for unmanaged-browser usage and for BYOD devices.The 15-domain posture framework maps cleanly onto these — see the posture review for the question set this document plugs into.
We help organisations move from ad-hoc AI usage to a defensible, audit-ready posture — managed-settings deployment via your existing MDM, DLP hook tuning, marketplace governance, incident playbooks, and the cross-tool view across Claude Code, Cursor, Copilot, Gemini CLI and Aider.