← All posts

Claude Code in a Devcontainer: Sandboxing Agents Properly (2026)

Anthropic's reference devcontainer does two things: filesystem isolation and a default-deny egress firewall. Most DIY Docker setups skip the second, which is the one that matters.

There is a permission prompt you have all pressed "yes" to a hundred times, and a flag that turns it off, and a widely shared piece of advice that you should never use that flag.

The advice is right, with one exception: inside a container built for it. Anthropic ships a reference devcontainer for exactly this reason, and understanding what it does — and what it does not do — is the difference between a sandbox and a false sense of one.

Why the prompts exist and why they get skipped

An agent that can edit files and run commands can do anything you can do. The permission prompt is the human checkpoint. It works, and on a long task it fires often enough that people stop reading it, which is the real failure mode. A checkpoint you approve reflexively is not a checkpoint.

The honest options are: keep reading every prompt, or remove the agent's ability to do damage and then stop prompting. The second is what containers are for.

What Anthropic's reference container actually does

The anthropics/claude-code repository includes a .devcontainer setup that is worth reading even if you never use it directly. Two design choices carry the weight.

Filesystem isolation. The container sees your project directory and nothing else. No ~/.ssh, no ~/.aws, no adjacent repos, no browser profile. An agent that goes wrong can wreck the checkout, and the checkout is in git.

A default-deny egress firewall. This is the part people miss and it is the more important half. The container runs an init-firewall.sh on startup that blocks all outbound network traffic by default, allowlists what the toolchain genuinely needs — the Anthropic API, npm, GitHub — and then verifies the rules are in place before proceeding.

That verification step is the detail that shows the threat model is real. Filesystem isolation stops an agent from deleting things. Egress control is what stops a compromised dependency or a prompt-injected instruction from sending your source code somewhere. Most homegrown "I run it in Docker" setups do the first and not the second, and the second is the one that protects against the attack you did not think of.

Because both are in place, that environment is where claude --dangerously-skip-permissions is a reasonable thing to run. The flag's name is doing honest work: outside a container it is exactly as bad as it sounds.

Getting it running

If your project has no devcontainer yet, the fastest path is to copy the reference one and adjust:

# from your project root
mkdir -p .devcontainer
# copy devcontainer.json, Dockerfile, and init-firewall.sh
# from github.com/anthropics/claude-code/.devcontainer

Open the folder in an editor with devcontainer support and let it build. Or drive it from the CLI:

npm install -g @devcontainers/cli
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . claude

The CLI path matters if you do not want to live in a specific editor — it means the container is a terminal thing, and your terminal is still your terminal.

Two settings you will want to change from the defaults:

Persist the history and config. Rebuilding the container should not wipe your agent's local state. The reference setup uses named volumes for this; keep them.

Add your own allowlist entries. If your build pulls from a private registry, an internal package proxy, or a company GitHub Enterprise host, add it to the firewall script. The default-deny posture means anything you forget shows up as a confusing network failure rather than a security warning — budget an hour the first time.

What containers cost you

I want to be even-handed, because there is a real tax.

Filesystem performance on macOS. Bind mounts across the VM boundary are slower than native, and on a big node_modules install or a Rust build you feel it. Named volumes for dependency directories help a lot:

"mounts": [
  "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
]

Your environment is gone. Your shell config, your aliases, your prompt, your git identity — none of it is in the container unless you put it there. Dotfiles support in devcontainers handles most of this, and it is worth setting up on day one rather than living in a bare bash prompt and quietly hating the setup.

Services need to be in the compose file. Postgres, Redis, whatever your tests need. This is a one-time cost that also happens to be a genuine improvement, since it makes the requirement explicit.

Debugging the firewall is unfamiliar work. When something cannot reach a host, the error is usually a timeout from a tool five layers down, not "blocked by policy."

The multi-container pattern

Once agents are containerized, running several stops being scary, because the isolation is per-container rather than a convention you are maintaining by hand.

One container per worktree, one agent per container, each on its own branch. This composes exactly with the worktree pattern in git worktrees and parallel agents — worktrees give each agent its own branch and directory, containers give each one its own filesystem and network policy.

The practical problem then becomes the boring one: with four containerized agents running, which one is waiting for you? Container logs do not bounce your dock, and hooks that fire osascript inside a Linux container do nothing at all.

The two workable answers:

The terminal bell. It travels through docker exec like any other byte, so a Stop hook that prints \a reaches your actual terminal. See the hooks guide.

Watch the session, not the process. MOLTamp's session monitoring works on the local side of the stream, so a tab running devcontainer exec behaves like any other session — amber when it has gone quiet waiting on input, green when a turn finishes. It does not need anything installed inside the container, which is the whole point when the container is deliberately minimal. Details in the 3.2 tab system.

Giving each container's session a visibly different skin helps more than it sounds like it should — four identical black rectangles is how you paste into the wrong one.

When not to bother

Solo work on a repo you own, with a normal permission workflow. If you are reading the prompts, the prompts are doing their job. The container is overhead.

Anything needing deep host access. Docker-in-Docker, hardware, VPNs, some GPU workloads. Possible, but you will spend more time on the container than the code.

Tiny repos and quick fixes. Two minutes of container startup for a three minute task is a bad trade.

The clear cases for containerizing: working in a repo you do not fully trust, running agents unattended, running more than two at once, or anything on a machine that also holds production credentials.

The short version

The reason to containerize is not tidiness. It is that filesystem isolation plus a default-deny egress firewall is what makes turning the permission prompts off a considered decision instead of a shortcut. Anthropic's reference devcontainer implements both, and the firewall half is the half most DIY setups skip.

Start from theirs, add your registry hosts to the allowlist, put your dotfiles in, and use volumes for dependency directories so the performance stays tolerable.

For the rest of the agent setup — shells, fonts, notifications — the complete Claude Code terminal setup covers the local side.

MOLTamp is a skinnable terminal shell built for agent work: up to 50 sessions, ⌘P search, per-session monitoring, and a community skin marketplace. Download it.