What to set before anyone logs in
- Deny rules stop Claude's own tools and the file commands it recognises, not a Python script that opens the file itself. They are not an operating-system boundary.
- The OS sandbox is the boundary. But its default read policy still hands over
~/.aws/credentialsand~/.ssh, so you have to deny those by hand. - Two of your safety controls fail open: a bad version pin is silently dropped, and a sandbox that cannot start runs your commands unsandboxed unless you tell it not to.
- On native Windows there is no sandbox at all. The boundary you are relying on does not exist until you move people into WSL2 or a container.
disableBypassPermissionsModedoes work. Set it. Just don't mistake the one control that holds for the whole job being done.
A team I was helping had done the responsible thing before rolling Claude Code out. They had a managed settings file with a tidy block of deny rules: Read(./.env), Read(~/.ssh/**), Read(~/.aws/**), the credential paths you would expect. Someone had clearly read a hardening guide. They felt locked down, and they said so.
Then I asked Claude Code, inside their own config, to run python -c "print(open('.env').read())".
It printed the file.
The deny rule never fired, because the deny rule was never going to fire. That is not a misconfiguration. It is the documented behaviour, and it is the single most important thing to understand before you hand this tool to a few hundred people: the controls that look like a lock are governing the wrong layer.
Deny rules govern Claude, not the operating system
Here is the sentence from Anthropic’s own permissions docs that the hardening guides skip. Read and Edit deny rules “apply to Claude’s built-in file tools and to file commands Claude Code recognizes in Bash, such as cat, head, tail, and sed. They do not apply to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself.”
So Read(./.env) stops Claude reading the file with its Read tool, and stops cat .env, because Claude Code knows what cat does. It does nothing about python -c "open('.env')", a Node script, a make target, or any of the thousand indirect ways a file gets opened. The deny list is a fence around Claude’s hands. It is not a fence around the file.
The same softness runs through the Bash rules. Anthropic labels argument-constraining patterns “fragile” in their own documentation, and they are right. A rule meant to pin curl to one host, Bash(curl http://github.com/ *), sails past curl -X GET ..., past https://, past a redirect through bit.ly, and past URL=http://github.com && curl $URL. Their recommendation is the right instinct: don’t try to allow a safe-looking curl. Deny curl and wget outright and route web access through WebFetch(domain:...) instead, where the domain match actually holds.
None of this means deny rules are useless. They shape what Claude reaches for, and that is worth having. It means they are a behavioural control wearing the costume of a security boundary, and a CISO who signs off on “we deny the credential paths” has been shown the costume.
The sandbox is the boundary, and it reads your whole machine
The thing that actually enforces at the OS level is the sandbox. It uses Seatbelt on macOS and bubblewrap on Linux, and crucially it binds every Bash command and its child processes, so the Python one-liner that walked through your deny rule hits a wall it cannot reason past. This is the control to build the baseline on.
And this is where the defaults turn on you. With the sandbox enabled, its default read policy is “read access to the entire computer, except certain denied directories.” Anthropic spells out the consequence in a note most people scroll past: “this default still allows reading credential files such as ~/.aws/credentials and ~/.ssh/. Add them to denyRead to block them.” So you turn on the sandbox, you feel safer, and your AWS keys and SSH private keys are still readable by anything running inside it. The boundary is real. The default boundary is drawn in the wrong place, and it is on you to redraw it.
Two more defaults deserve to be on a runbook in red ink.
The sandbox does not run on native Windows. Not “runs with reduced features.” It is macOS, Linux, and WSL2 only. If your fleet is Windows laptops, the OS-level control you are leaning on does not exist until you put people inside WSL2 or a container, and a deployment plan that assumes the sandbox is protecting a Windows estate is protecting nothing.
And the sandbox’s network filter does not inspect TLS. It decides allow or deny from the hostname the client hands it, which means code inside the sandbox can use domain fronting to reach a host you never allowed by addressing it as one you did. If your threat model needs a real network boundary you have to front the sandbox with a proxy that terminates and inspects TLS, which is the same corporate proxy I wrote about in making Claude Code trust your inspection layer. The two posts meet here: the sandbox is your local boundary, the inspecting proxy is your network one, and neither covers the other’s gap.
The controls that fail open
Most security settings fail closed. If they break, they deny. Two of the ones you will rely on most do the opposite, and the asymmetry is the kind of thing that turns a clean audit into an incident.
requiredMinimumVersion is the real control for fencing out builds with known vulnerabilities. Set it in managed settings and Claude Code refuses to start below the floor, while claude update and claude doctor keep working so people can self-recover. It is a good control, far stronger than minimumVersion, which only governs auto-update and never blocks anything. But per the docs it “fail[s] open by design: an invalid value is stripped rather than enforced, so a bad policy push cannot prevent Claude Code from starting.” Fat-finger the version string in your MDM push and the fence is silently gone. Nothing errors. Everyone keeps working on whatever build they had, including the one you were trying to fence out.
The sandbox fails open the same way. Out of the box, if it cannot start, because a Linux box is missing bubblewrap or someone is on an unsupported platform, Claude Code “shows a warning and runs commands without sandboxing.” The warning scrolls past in a terminal nobody is reading. The fix is one line, failIfUnavailable: true, which turns “can’t sandbox, carry on” into “can’t sandbox, won’t start.” If you are treating the sandbox as a security gate and you have not set that, you do not have a gate. You have a suggestion that disappears the first time a dependency is missing.
There is a third gap with the same shape, and it is the one that unsettles CIOs most when I show it. When a command fails inside the sandbox, Claude Code can analyse the failure and retry it with a dangerouslyDisableSandbox parameter, outside the boundary. That retry goes through a permission prompt, so it is not silent, but it means the model treats your sandbox as an obstacle to route around rather than a law to obey. To take the option away you set allowUnsandboxedCommands: false, which the docs call Strict sandbox mode. Until you do, your isolation has a documented escape hatch the agent already knows how to reach for.
Set the controls that do hold
I have spent four paragraphs on what leaks, so let me be just as clear about what works, because the answer to silent gaps is never to throw the controls out.
disableBypassPermissionsMode works. Set it to the string "disable" in managed settings and it removes bypass mode and rejects --dangerously-skip-permissions, the flag any developer can otherwise use to skip every check you built. There was loose talk that this control was broken; it is not, it is in the docs and it does what it says, and in managed settings no user can override it. Pair it with disableAutoMode set the same way.
Identity holds too, and it is where the baseline should start. forceLoginMethod and forceLoginOrgUUID in managed settings block sessions authenticated by a raw ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN and pin login to your organisation, so a personal account or a loose key cannot stand in for the managed one. That is the account-level version of the same fight I made in blocking the personal account. And allowManagedPermissionRulesOnly: true stops users and projects adding their own allow rules on top of yours. Managed settings sit at the top of the precedence chain and cannot be overridden by user settings, project settings, or even command-line arguments, which is the property that makes any of this enforceable rather than advisory.
Lay it in order, then prove it
The order matters, because each control assumes the last one is in place. Identity first: SSO, then forceLoginMethod and forceLoginOrgUUID so only managed accounts start a session. Deploy the managed settings file through your MDM before anyone touches the tool, and validate the JSON, because a malformed managed file is ignored rather than enforced, which is fail-open again. Pin the version. Kill bypass and auto mode. Lay the deny baseline and lock it with allowManagedPermissionRulesOnly. Restrict which MCP servers can load, which is its own discipline. Then turn the sandbox on properly: enabled, failIfUnavailable: true, allowUnsandboxedCommands: false, and a denyRead for the credential paths the default leaves open. Wire telemetry to your SIEM so there is an audit trail. Then pilot it against real work and watch what the controls actually block before you widen anything.
That last step is not optional politeness. The thread running through every gap above is the same one running through this whole cluster: the control that looks like security and the control that is security are different objects, and silent failure is the villain. A deny rule that does not deny, a version pin that drops itself, a sandbox that waves the command through, none of them announce the lapse. The only way to know your baseline holds is to attack it yourself, with the exact profile you are about to ship, and confirm the thing you blocked is actually blocked.
This is the floor that the rest of phase zero stands on, and it is the install-time companion to the broader argument that Claude Code security is a design problem, not a settings file. Set the baseline. Then assume every line of it can fail quietly, and go prove the ones that matter.





