Security How-to

Your browser tests fail inside Claude Code's Linux sandbox. Pick the fix that costs the least isolation

Chromium and rootless Podman can't create user namespaces in the sandbox. Three documented fixes exist, and one of them switches off a security boundary.

Five floating glass layers stacked vertically, with a small gold light passing through a narrow slot in the solid middle layer

Ask Claude Code on Linux to run your browser tests with the sandbox on, and the first run ends with Chromium exiting on No usable sandbox!. The same wall stops rootless Podman and anything else that wants to create a user namespace. It isn’t a bug. The Claude Code sandbox forbids it by default, and the docs list three ways round it.

They aren’t equivalent. One only weakens the browser. One lets the agent run arbitrary code with your full access. One removes a boundary the rest of the sandbox leans on. This piece covers what each does, which to pick and how to stop a wrong choice spreading to the whole team.

Everything below comes from the sandboxing docs and the changelog . It describes documented behaviour and wasn’t run end to end, because the sandbox needs bubblewrap and a kernel that allows it.

What’s failing

On Linux and WSL2, Claude Code wraps shell commands in a bubblewrap sandbox. Commands inside it can’t create user namespaces by default . Chromium wants one for its own renderer sandbox, so it refuses to start.

The docs list the symptoms, so you can recognise them in a transcript:

  • Chromium exits with No usable sandbox!
  • unshare -U true prints unshare: unshare failed: Operation not permitted
  • Rootless Podman prints cannot clone: Operation not permitted, or stops earlier with read-only file system on one of its own folders

The unshare line is the cheapest probe. Run unshare -U true through Claude Code and through a plain terminal. If it only fails in the first, the sandbox is the cause, not your kernel or AppArmor.

That distinction matters because Ubuntu 24.04 and later have a separate restriction. The docs tell you to check sysctl kernel.apparmor_restrict_unprivileged_userns and, if it returns 1, add an AppArmor profile that lets bwrap itself create user namespaces. That’s a setup step for the sandbox to start at all. It isn’t a fix for nested namespaces.

The three fixes

Each fix works for the tools it names and gives up some isolation. Here they are in order of how much they give up, which is not the order the docs list them.

Start the browser with –no-sandbox

For Chromium or Electron, pass --no-sandbox to the browser. The Claude Code sandbox stays exactly as it was . What you lose is Chromium’s own sandbox, so the browser runs web content without it.

That’s a narrow loss when the page under test is your own app on localhost. It’s a bad trade if the agent also uses the browser to open pages you don’t control, because a renderer exploit then has nothing between it and the process’s privileges. The outer Claude Code sandbox still applies to that process, which is the point of leaving it alone.

Where the flag goes depends on your test runner. Look in its docs for the browser launch arguments, and set the flag there so the test command stays unchanged.

Exclude the test command from the sandbox

The second fix is a pattern in sandbox.excludedCommands. Matching commands run outside the sandbox with no filesystem restrictions and no network proxy . The docs use this for rootless Podman, Buildah, and any script or test command that runs a browser or unshare.

{
  "sandbox": {
    "enabled": true,
    "excludedCommands": ["npx playwright test *"]
  }
}

The matching rules are fussy, and they decide whether the entry does anything:

  • End the pattern with *. Without a wildcard it’s an exact match, so npx playwright test matches only the bare command.
  • Every command in a call has to match. npm ci && npx playwright test stays sandboxed unless another entry covers npm ci.
  • Claude Code matches the text of the call. A make target or script that runs the tests internally doesn’t match.
  • A redirect to a file, a cd or a $(...) substitution keeps the whole call sandboxed.

So match the test command, not the browser. The docs say so directly: for a browser, the pattern has to match the command Claude runs.

Now the cost. The docs warn that Claude can edit a script in your working directory before running it. With an entry like the one above, an agent can edit a test file and then run it with your full access, no filesystem limits and no network proxy. The entry doesn’t excuse the command from permissions entirely: it goes through the regular flow, where read-only commands and commands your allow rules cover run without a prompt, auto mode’s classifier reviews the rest, and bypassPermissions runs it without a prompt unless an ask rule matches.

To confirm an entry matches, switch to Manual mode and ask Claude to run a matching command that changes something. The prompt is titled “Bash command (unsandboxed)”. If you see that title, the entry worked. If the command runs without it, the pattern is wrong or a broader rule is approving the command.

Allow nested user namespaces

The third fix is sandbox.allowNestedUserNamespaces, added in Claude Code 2.1.305 on 20 October 2026. It keeps Chromium’s own sandbox working and also allows a nested bubblewrap or unshare. It goes in ~/.claude/settings.json, not the project’s .claude/settings.json.

{
  "sandbox": {
    "enabled": true,
    "allowNestedUserNamespaces": true
  }
}

It’s the tidiest fix and the one with the sharpest warning. The docs say that setting it to true turns off a security boundary : code from a sandboxed command can end up running outside the sandbox. They also say not to rely on any of the sandbox’s restrictions, including protected paths and credentials.files entries, to stop a malicious command, and that in auto-allow mode such a command can run without a permission prompt.

Protected paths are the files a sandboxed command can’t write even inside a writable directory: the .claude settings files, the skills, agents, commands and hooks directories, .mcp.json, shell startup files, .gitconfig, and hooks and config inside .git. They exist so a command can’t grant itself permissions or plant a hook. With this setting on, that protection is no longer something to count on.

It also doesn’t finish the job for everything. The docs say the setting alone isn’t enough for rootless Podman or Buildah, which the sandbox stops in other ways too. A command that begins with unshare stays sandboxed whatever excludedCommands pattern you list, because unshare can start any other program.

Which one to pick

This part is opinion, built on the trade-offs above.

For browser tests against your own app, use --no-sandbox in the browser launch arguments. It’s the only fix that leaves the Claude Code boundary and your permission prompts untouched, and the thing it weakens is a renderer sandbox around pages you wrote.

Reach for excludedCommands when the tool really can’t live in the sandbox, as with Podman, and keep the pattern as specific as the command allows. Treat every excluded command as code the agent can rewrite and then run as you. If the same repository has an agent that writes tests, that’s a real exposure.

Leave allowNestedUserNamespaces off unless you need Chromium’s own sandbox or nested bubblewrap, and talk to whoever owns security first. The docs say the same: if your organisation has a security team, check with them before turning it on.

There’s one case where none of the three applies. With CLAUDE_CODE_SUBPROCESS_ENV_SCRUB set to 1, every command runs sandboxed and the settings that the other fixes name have no effect. There, start Chromium or Electron with --no-sandbox and run any other affected tool yourself, outside Claude Code.

Keep a personal workaround personal

allowNestedUserNamespaces is scoped to user or managed settings, and a repository can’t turn it on . A cloned repo can’t flip it for you through a committed .claude/settings.json. That’s the reason the docs tell you to put it in your home directory.

excludedCommands is different. It works from any settings file, so a committed entry applies to everyone who clones the repo, unless the sandbox is admin-required, in which case Claude Code ignores entries in .claude/settings.json and .claude/settings.local.json. Review changes to that file the way you’d review a CI workflow.

For a team, the docs give a managed configuration that turns the sandbox on, refuses to start when it can’t run, and stops the model retrying outside it:

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false
  }
}

The docs add that allowNestedUserNamespaces is one of the keys a developer’s user settings can turn on unless managed settings set it, and that you should set it to false in managed settings if you don’t want it used.

The retry that makes this worse

When a command fails in the sandbox, Claude may retry it with the dangerouslyDisableSandbox parameter. The docs describe this retry as the escape hatch for commands that fail inside the sandbox, which is where a browser that can’t start leaves a model looking for a way forward.

What happens next depends on your mode. In bypassPermissions the retry runs without a prompt. In Manual and acceptEdits you get the “Bash command (unsandboxed)” prompt. In auto mode a classifier looks at the underlying command. A matching allow rule such as Bash(npx *) also approves the retry, so the command runs outside the sandbox with no prompt at all.

Two things close this. Add an ask rule for Bash(dangerouslyDisableSandbox:true), which prompts on every retry, including in auto and bypassPermissions modes, and takes precedence over a matching allow rule. Or set "allowUnsandboxedCommands": false, which makes Claude Code ignore the parameter altogether.

First 15 minutes

  1. Run unshare -U true through Claude Code and in a plain terminal. If only the first fails, the sandbox is what’s blocking namespaces.
  2. Search your settings for anything already loosening it: grep -rn "allowNestedUserNamespaces\|excludedCommands" ~/.claude/settings.json .claude/settings.json.
  3. Add --no-sandbox to the browser launch arguments in your test config, and rerun the tests through Claude Code.
  4. Add the ask rule Bash(dangerouslyDisableSandbox:true) to your user settings, so a failed command can’t quietly leave the sandbox.
  5. Run /sandbox and open the Config tab to see the protected paths the sandbox resolves for your machine.