Six traps in Claude Code's sandbox

An agent that writes code has to run commands. Install the dependencies, build, run the tests. So the first thing most teams do is write a list of allowed commands.

The list never holds. It fits the repositories it was written for and nobody else's. Another language, another build tool, a script with another name, and the agent stops. Pipes are refused, so is $?, so is a redirection. Every denial pulls a person back to the keyboard, and soon the list grows until it allows almost anything.

A list says what a command may be. A sandbox says what any command may touch, which is the question you actually care about.

Claude Code ships one. Seatbelt on macOS, bubblewrap on Linux. Here is how to turn it on so it cannot be turned off, and six traps worth knowing before you trust it.

Turn it on, and make it impossible to bypass

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false
  }
}
  • enabled is off by default. Every developer has to turn it on, unless your company deploys managed settings.
  • failIfUnavailable, because without it, when the sandbox cannot start, commands simply run without one.
  • allowUnsandboxedCommands: false, because without it, a command that fails inside the sandbox may be retried outside it.

Trap 1. The sandbox only covers the shell

The sandbox confines Bash and the processes it starts. Claude's own Read, Edit and Write tools follow permission rules instead. A file hidden from cat is still readable by Read.

So every path you hide from the sandbox, hide from the tools too.

"permissions": {
  "deny": ["Read(//**/.env)", "Read(//**/.envrc)", "Read(//Users/you/.ssh/**)"]
}

The // means the root of the machine. A rule on ./** is relative to the current folder, and moves the moment the agent runs cd.

Trap 2. A writable .git runs code outside the sandbox

The sandbox lets a command write in its working folder. If that folder holds a .git, a command can write a hook or a line of git configuration. Nothing runs yet. Then you, or your editor, run git status outside the sandbox, and git runs what the agent wrote.

Deny writes to every .git, and to your own git configuration.

"denyWrite": ["/**/.git", "/Users/you/.gitconfig", "/Users/you/.config/git"]

Trap 3. Your git login still works inside

Hiding ~/.ssh and ~/.git-credentials is not enough. On macOS the keychain can still answer from inside the sandbox, so git finds the login anyway.

The reliable fix is to cut the agent off from your code host entirely, with deniedDomains on its hosts, and let a process outside the sandbox push for it, one branch and never forced. The cost is real, a dependency fetched from your code host will not install inside the sandbox. Leave that check to your CI.

Trap 4. Globs that work on macOS, not on Linux

On macOS, /**/.env hides every .env on the machine. On Linux the sandbox can skip the rule. One fix is to find those files before the session starts and list each one by its full path. A file created later is still hidden from the Read tool, but not from the shell. Know which of the two you are running on.

Trap 5. Rules that look right but are ignored

  • A Write(path) rule is ignored. Claude Code says so in its output, if you read it. Use Edit(path), which covers every tool that writes a file.
  • Deny rules win over allow rules. If you deny .env.* you cannot allow .env.example back. Write the deny pattern so it skips it.

Trap 6. Every MCP server of your account is in reach

Claude Code loads servers by itself, the ones you configured and every connector of your account. An agent given one tool may reach for another whose name looks alike. Mail, drive, chat, whatever your account connects, is in reach of every session.

Deny each server the task does not need, whole.

"deny": ["mcp__claude_ai_Gmail", "mcp__claude_ai_Google_Drive"]

A denied server's tools are not offered to the agent at all, so it cannot even try.

The whole thing

{
  "permissions": {
    "deny": [
      "Read(//**/.env)",
      "Read(//**/.envrc)",
      "Read(//Users/you/.ssh/**)",
      "WebFetch",
      "mcp__claude_ai_Gmail"
    ]
  },
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "autoAllowBashIfSandboxed": true,
    "network": {
      "allowedDomains": ["*"],
      "deniedDomains": ["github.com", "gitlab.com", "bitbucket.org"]
    },
    "filesystem": {
      "allowWrite": ["/Users/you"],
      "denyWrite": ["/Users/you/.ssh", "/Users/you/.gitconfig", "/Users/you/.config/git", "/**/.git"],
      "denyRead": ["/Users/you/.ssh", "/Users/you/.git-credentials", "/Users/you/.netrc", "/Users/you/.config/gh", "/**/.env"]
    }
  }
}

Replace /Users/you with your home, and keep in deniedDomains only the code hosts you use. autoAllowBashIfSandboxed lets commands run without asking you, because the sandbox, not a prompt, is now the boundary. The home stays writable because every toolchain keeps its caches there. The network stays open because every project's checks reach different registries.

Bring your team into the beta.

Every request is read and answered by hand.