To keep Claude Code away from your secrets and the code that watches it, you need two things: deny rules in a .claude/settings.json file, and the sandbox switched on. Neither is on by default.
We set up both in a throwaway repo on 22 October and told Claude Code to get past them. It couldn’t. Here is the exact file, what each line does, and the gaps you still have to cover yourself.
What you need first
- Claude Code. We tested version 2.1.306. Run
claude --versionto check yours. - macOS, Linux or WSL2 for the sandbox. The docs say: “On native Windows, Claude Code runs commands unsandboxed.” On Linux and WSL2, install
bubblewrapandsocatfirst. - A repo you can commit a settings file to. The file goes in
.claude/settings.jsonat the root.
Check which permission mode you’re in
Recent versions start in auto mode. From the docs: “With Claude Code v2.1.283 or later, auto mode is the built-in starting permission mode for interactive terminal and VS Code sessions.” Auto mode runs actions without asking, with background safety checks.

Press Shift+Tab to cycle modes. The good news is that your rules hold whatever mode you pick. The docs say: “Deny rules block in every mode, including bypassPermissions.” That’s why rules matter more than modes.
Write deny rules for your secrets
Create .claude/settings.json with this:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"deny": ["Read(.env)", "Read(.env.*)", "Read(~/.ssh/**)", "Edit(monitoring/**)"],
"ask": ["Bash(git push *)"]
},
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false,
"filesystem": { "denyWrite": ["./monitoring"] }
}
}
The path rules use gitignore-style patterns, and the prefix matters:
Read(.env)with no prefix matches a.envat any depth under the current directory. The docs: “Bare filenames follow gitignore semantics and match at any depth”. In our test,Read(.env.*)also blockedconfig/.env.local.Read(./.env), in the docs’ words, “Matches reading the.envfile in the current directory”.~/is your home folder, soRead(~/.ssh/**)covers everything in.ssh.- A single leading
/is relative to the settings source (for a project file, the folder you start Claude Code in), not the root of your disk. Use//for a true absolute path.
A Read deny also blocks Claude’s Edit and Write tools on the same path. Settings files are strict JSON: a comment or a trailing comma is a syntax error.
Protect the code that watches your agent
Edit(monitoring/**) stops Claude Code editing your monitoring folder. Because it has no prefix, it also matched tests/monitoring/ in our test. Swap in the folders that hold your alerting, audit logging or security checks, and their tests.
Use Edit, not Write, for path rules. The docs warn that for a Write path rule, Claude Code “accepts the rule but never consults it”. This is exactly the kind of change that went wrong in OpenAI’s two agent incidents, where agents weakened monitoring in the code they were editing.
Turn on the sandbox
Deny rules have limits, and the docs are clear about them. They cover Claude’s own file tools, and recognised shell commands such as cat, sed and tee. They don’t cover “a Python or Node script that opens files itself”.

That’s what the sandbox is for. It’s an operating-system-level fence around the shell commands Claude runs, and “The sandbox is off by default.” The sandbox block in our file does three things:
"enabled": trueturns it on. You can also run/sandboxin a session."allowUnsandboxedCommands": falsestops Claude Code retrying a blocked command outside the sandbox."denyWrite": ["./monitoring"]blocks shell writes into the monitoring folder. Your Read and Edit deny rules are merged into the sandbox too.
One warning worth acting on: if the sandbox can’t start, Claude Code runs commands without it by default. If you’d rather it refused, the docs’ sandbox.failIfUnavailable setting makes Claude Code exit at startup instead. We didn’t test that setting.
Make risky commands ask first
"ask": ["Bash(git push *)"] makes Claude Code ask before a git push. Ask rules still prompt in auto mode.
Treat Bash rules as a seatbelt, not a lock. The docs say a Bash rule “isn’t a security boundary around the program”, and give an example. Bash(git push *) doesn’t catch git -C . push origin main. For anything that must never happen, rely on the sandbox or a hook, not on matching command text.
Check what loaded
Before you trust the file, check it took effect:
/permissionslists every rule and the file it came from./statusshows which settings files loaded.claude doctorlists any entries Claude Code rejected./sandboxshows the sandbox’s configuration.
What happened when we tested it
We ran Claude Code 2.1.306 on macOS in a throwaway repo, with fake secrets. Each test was a single non-interactive run (claude -p) in acceptEdits mode, so the agent could edit files without asking.
We made ten attempts to read the secrets, edit the monitoring code or write outside the project. All ten were blocked. The git push was refused too: in a non-interactive run, an ask rule can’t show its prompt, so the command doesn’t run.
Then we removed the settings file and repeated two tests. Claude Code read .env and edited the monitoring file.
The messages you’ll see:
- From a deny rule:
File is in a directory that is denied by your permission settings. - From the sandbox:
Operation not permitted.
One detail surprised us. On its first try, without being told .env was a test file, the model declined to open it from a Python script. With that context, it tried, and the sandbox stopped it. Don’t rely on the model’s judgement: the settings are what held every time.
What goes wrong
- The sandbox fails open. On an unsupported platform, or without its dependencies, commands run unsandboxed unless you set
failIfUnavailable. Writepath rules do nothing. UseEditfor paths.- Bash rules miss rewritten commands. Back them with the sandbox.
bypassPermissionsis for throwaway machines. Deny rules still apply, but the docs say to use it only “in isolated environments like containers, VMs, or dev containers without internet access”.
Going further: production codebases
This setup protects one developer’s machine. On a production codebase, especially in a regulated business, treat it as the first layer of several. The docs are explicit that CLAUDE.md instructions aren’t enforcement: “Permission rules are enforced by Claude Code, not by the model.” So stack layers the agent can’t talk its way past:
- Read-only agents. For review or investigation work, the
dontAskmode denies anything that would prompt. The docs pitch it for “Locked-down CI and scripts”. - Network. The sandbox starts with no allowed domains. Add only the ones you need: the docs warn that “Allowing broad domains such as
github.comcan create paths for data exfiltration.” - Credentials. “There is no built-in credential deny list”, so list the files and environment variables to block. Better still, keep production credentials off the machine entirely.
- Tools outside the sandbox. Local MCP servers and hooks “run with your full access”. Deny MCP tools you don’t use, with rules like
mcp__*. - Rules nobody can switch off. For a team, put the rules in managed settings, which no other level “including command line arguments” can override. They can also disable
bypassPermissions. For hard blocks on commands, add a PreToolUse hook: exiting with code 2 stops the action even if an allow rule would let it through. - Git, enforced on the server. Branch protection, required reviews and a bot account limited to opening pull requests hold even when a local rule is bypassed.
For how other developers set up their agents, see how leading developers code with AI agents.