No results

For AI agents and crawlers: a structured index of this site is available at https://danny.is/llms.txt.

Claude Code's Bash Sandbox

A few months ago I threw together denv as an interim solution to sandboxing local Claude Code sessions. My main requirement was that I could have a local project directory open in a terminal and VS Code (or any other local app I want) while running the Claude Code TUI on the same project dir in a sandbox.

Although I spent a lot of time digging into the various options (dev containers, various VMs etc), I settled on a pretty simple setup in the end. Running denv create ~/dev/my-project creates a Docker container from a denv-base and bind-mounts the project directory to /workspace in the container. The Dockerfile just installs a bunch of runtimes and developer tooling, writes some basic dotfiles which make the shell a little bit like my local environment and sets up some “global” Claude stuff, including some hooks to prevent certain write operations without my permission. I could then run denv <project-name> to SSH into the container and run claude in a generally similar environment to my local one. For new projects, denv scaffold <name> scaffolds an empty project in my ~/dev/ directory and creates the container, while a few convenience commands basically just wrap docker: denv start, denv stop, denv ls and the like.

The whole idea was that:

  1. Claude can work without restrictions because the only thing it can mess with on my actual machine is the project files.
  2. It can also have fairly permissive read access to the internet via its own tools and CLI tools like gh, which it needs to do research, but needs my permission for external write operations like pushing to git remotes or creating GitHub issues.
  3. I still feel like I’m working locally in every other respect.

This worked pretty well, and is still a decent solution for any Claude sessions I wanna run with very open permissions. But increasingly I’ve found myself just running Claude Code locally in ~/dev/my-project/ because it’s just easier and I’m lazy. Using denv containers means:

  • The project needs to have one, or I gotta create it (which also means remembering to run OrbStack).
  • Each container has its own isolated config for both its shell and Claude Code. Even if I remember to mirror changes to my local setup in the denv repo, I need to denv rebuild and maybe denv update-plugins before the containers get those changes.
  • Because the containers are intentionally long-lived (so Claude can install project-specific stuff it wants), it’s too easy for them to diverge from the base config-as-code in ways that are awkward to maintain.1
  • A lot of my projects include macOS or Tauri apps and need to run some stuff on my actual mac, or access data in places like ~/Library/Application Support/MyApp/ or whatever, which can be awkward since agents running in the container can’t access them without explicit setup from me.

What do I actually want from a “sandbox”?

If I was running agents in YOLO mode I’d want a very tight sandbox, but since I mostly run Claude Code with some oversight what I really want is this:

  1. Claude runs locally. Running claude in a project directory should Just Work™ – it can still access files, processes and tooling only available on my local machine when necessary.
  2. Without full access to everything. The Claude process does not have free rein over my local machine by default because most of the time that’s unnecessary.
  3. Obviously dangerous stuff is blocked. Claude cannot read sensitive files (keys, secrets etc) or run dangerous commands on my machine.
  4. Few permission prompts for “normal” work. Claude can work freely within the project directory without constantly asking my permission for the kinds of things I’m always gonna say “yes” to.2 This probably means restricting write operations more than read operations.
  5. When in doubt, ask me. Anything not blocked by (3) or allowed by (4) should fall back to asking my permission.

Claude’s Bash Sandbox

Claude Code has a number of different sandboxing options, and the most interesting for me is the Bash sandbox, because it’s the only one that leaves me working locally. It ensures the Bash tool always runs inside an OS-enforced Seatbelt sandbox. It only covers Bash tool calls so other built-in tools like Read, Edit, and WebFetch aren’t covered, nor are MCP tools.

When a sandboxed command is blocked – because it touched a denied path or host – Claude can retry it “unsandboxed”, which triggers a permission prompt. That denial → retry → prompt flow is how it reaches out of the sandbox and asks. Worth knowing that approving one runs the whole command unsandboxed, not just the bit that was blocked.

Configuration

You can find my current Claude settings.json in my dotfiles repo:

Permissions

Claude’s permission rules govern everything the Bash sandbox doesn’t: the other tools, MCP, and any command running outside the sandbox. deny and ask rules also survive the sandbox, so they apply to sandboxed commands too. They’re set up like this:

  • Allow - Read-only bash commands like ls git status and the like are always allowed, along with MCP(context7:*) and a few specific Skill tool calls. Explicitly allowing these means I’m never prompted for them.
  • Ask - Commands which I always want to manually confirm. While this is the default outside the sandbox, explicitly listing certain bash commands here ensures I’m asked even inside the sandbox. These are mostly things like Bash(gh pr create:*).
  • Deny - Bash commands which I always want to be denied – mostly OS stuff like Bash(mount:*), but also includes Read for sensitive files like Read(~/.ssh/**).

There’s one ask rule doing more work than all the others:

"Bash(dangerouslyDisableSandbox:true)"

Without it a command like cat ~/.ssh/id_ed25519 fails inside the sandbox, Claude offers to retry unsandboxed, and my own read-only Bash(cat:*) allow rule waves it through. This rule ensures every escape from the sandbox prompts me regardless. The mechanism is interesting too: ask rules with a pattern inside the parens are checked before anything else, but a bare Bash or Bash(*) rule isn’t, and gets skipped entirely for sandboxed commands.

Bash Sandbox

Inside the Bash sandbox, I allow network access to certain domains like registry.npmjs.org which are required for tools like npm to work, as well as localhost and *.danny.is. Filesystem access from the sandbox is set up like this:

  • denyRead - Everywhere I don’t want the sandbox to read, including stuff like ~/Desktop, ~/Library. For the occasions where I do want to allow access to these, Claude can always ask my permission to run the command unsandboxed.
  • denyWrite - A similar list of directories I don’t ever want sandboxed bash to write to.
  • allowRead and allowWrite - Directories where read or write access is explicitly allowed from the sandbox, mostly overriding certain things denied by the above. Includes directories like ~/Library/Caches, ~/Library/pnpm and ~/Library/Developer.

Certain credentials and environment variables are also protected with a deny list.

Excluded Commands

I’ve intentionally excluded certain commands via excludedCommands so they always run outside the sandbox, governed by the permission rules above instead. I do this for two categories of command:

  1. Things which can never work sandboxed, so the sandboxed attempt is just a dead step before the inevitable prompt. git push over SSH is the obvious one – ~/.ssh is deny-read by design – as are ps and lsof, since the sandbox is process-blind. Most of these have allow rules too, so they don’t prompt at all.
  2. Commands which I always want to approve with a prompt, like kill and docker. These are excluded without an allow rule, so they prompt me.

Additions to CLAUDE.md

I’ve updated my global CLAUDE.md with a brief mention that we are likely running in the Bash sandbox, and I’ve also temporarily added the following to help with debugging sandbox-related issues.

Markdown
  • If you are running with the bash sandbox enabled and have issues you suspect are because of it, tell the user before trying any complex workarounds. Common tells: EPERM on paths under ~/Library, a tool silently using a project-local cache/store it normally wouldn't, or a tool resolving to a different version than usual (check command -v — sandbox denies can make the shell fall through to a different binary on PATH).

I expect to remove this after ironing out any teething problems.

How’s it going?

After using this setup for a couple of weeks in manual acceptEdits mode I’m pretty happy with how things are working. It feels like I’m seeing a similar number of permission prompts as before, but with a much less risky setup. Crucially, the vast majority are about running unsandboxed commands which genuinely deserve my attention. There’s been a notable decrease in permission prompts where I’d always say yes.

The only downsides I’ve noticed so far are related to the tighter permissions rather than the Bash sandbox, so I expect to do some tweaking over the next few weeks.

Enabling Auto Mode

I always intended that this stuff would eventually provide a marginally safer environment in which to run auto mode locally, so when Anthropic recently made auto mode the default I decided to switch it on after a few weeks using the new sandbox and permissions without it.

What the classifier actually governs is pretty narrow because the sandbox gets there first – anything that can be sandboxed is auto-allowed without the classifier ever seeing it. So its actual surface is the excludedCommands, requests to non-allowlisted domains, and the non-Bash tool calls. Crucially it does not get to judge unsandboxed retries, because that dangerouslyDisableSandbox ask rule is evaluated before it.

Wrapping up

I’ve only been using auto mode with this setup for a few days, so I fully expect to fiddle with a bunch of stuff before I find the right safety vs autonomy balance for local dev work.

I don’t think we’ve solved the whole sandboxing issue yet – at least not in a way which adequately addresses both agent isolation and developer experience. Until we do, I expect I’ll continue to use both denv and Claude’s sandboxed runtime in certain situations.

Footnotes

  1. I could obviously solve this by having all my projects manage their own dev container config as code and rebuild on every new session from that and a base image.

  2. Asking the user to approve a bash command is basically pointless in August 2026 – modern agents generate such complex compound commands that any normal user will just smash Yes unless the first ~20 characters are obviously scary.