# Claude Code in a dev container: three layers, and what they do not protect

Canonical URL: https://zero2vibecode.com/blog/claude-code-dev-containers
Date: 2026-10-20
Tags: claude-code, security

A dev container holds the whole Claude Code process, not just its shell commands. Add the CLI with one feature line, keep your login across rebuilds with a named volume, and save runs without prompts for the reference container with a firewall and a regular user.

The sandbox in Claude Code restricts shell commands. That is a real boundary, but a narrow one: the file-editing tools, MCP servers and hooks still run on your machine. A dev container moves the boundary outward. The **whole** Claude Code process lives inside a Docker container your editor connects to — on your computer or in a cloud service such as GitHub Codespaces — and your edits reach the repository through a mount.

This is the fifth post in a series on the [Claude Code: Pro Level](https://zero2vibecode.com/learn/claude-code-pro) course, based on the elective lesson [Claude Code in a dev container](https://zero2vibecode.com/learn/claude-code-pro/6-1-dev-containers). If the sandbox is new to you, start with [the sandbox lesson](https://zero2vibecode.com/learn/claude-code-pro/1-3-sandbox) — the container builds on it.

## Three layers

Each layer builds on the one before, and you can stop at whichever one you need:

| Layer | What it gives you |
|---|---|
| The Claude Code feature | the CLI inside any dev container |
| A named volume at `~/.claude` | your login survives a rebuild |
| The reference container | a regular user and a firewall — a place for running without prompts |

## Layer 1: the feature

One line in `.devcontainer/devcontainer.json` adds Claude Code to a container:

```json
{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/devcontainers/features/node:1": { "version": "lts" },
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
  },
  "postCreateCommand": "sudo apt-get update && sudo apt-get install -y bubblewrap socat"
}
```

The catch is the `:1.0` tag. It pins **the feature's install script**, not Claude Code. The feature installs the latest release, which then updates itself. And `postCreateCommand` is there for a reason: without `bubblewrap` and `socat`, the Bash sandbox does not work on Linux.

## Layer 2: a login that survives a rebuild

Sign in to Claude Code, rebuild the container, and `claude auth status` returns code 1. A rebuild keeps the project folder but throws away the home folder, and `~/.claude` with it. Codespaces behaves the same way: after **Rebuild Container**, `/workspaces` is still there and your login is not.

The fix is a Docker **named volume** mounted at `~/.claude`, plus the environment variable `CLAUDE_CONFIG_DIR` pointing to the same path. The volume lives separately from the container, so the login and settings inside it outlast a rebuild:

```json
"mounts": [
  "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume"
],
"containerEnv": {
  "CLAUDE_CONFIG_DIR": "/home/node/.claude"
}
```

Login still disappearing? Check that the volume's `target` and `CLAUDE_CONFIG_DIR` name **exactly** the same path.

## Layer 3: the reference container

The Claude Code repository ships a reference `.devcontainer/` of three files: `devcontainer.json`, `Dockerfile` and `init-firewall.sh`. It adds two things:

- **a regular user**, `node`, instead of root;
- **a firewall**: outbound traffic is allowed only to the addresses listed in `init-firewall.sh`.

This is the container the documentation supports for runs with `--dangerously-skip-permissions`. Running as root, the CLI refuses that flag outright.

Before the first build:

1. **Read all three files.** `init-firewall.sh` runs as root on every container start, as the `postStartCommand`.
2. **Add the login domains.** Next to `api.anthropic.com`, the list needs `claude.ai` and `platform.claude.com`, or signing in and refreshing the login will hit the firewall. Need PyPI or a private package registry? Add it and rebuild.
3. **Pin the version.** Replace `latest` in the `CLAUDE_CODE_VERSION` build argument with a specific version, and add `"DISABLE_AUTOUPDATER": "1"` to `containerEnv`.

To check the firewall, make two requests from inside the container: one to `example.com` should fail, one to `api.github.com` should go through.

## No prompts, but with limits

Once all of that checks out, one unattended run inside the container — with the same limits the course teaches in [unattended runs with limits](https://zero2vibecode.com/learn/claude-code-pro/3-2-bounded-unattended-runs):

```bash
claude -p "Add a one-line description of the repository at the top of README.md" \
  --dangerously-skip-permissions --model sonnet --max-turns 10
```

A cheaper model, a cap on turns, a small task. The container is the reason you may skip prompts here; it is not a reason to skip the limits.

**What the container does not protect.** 
A container does not stop the project from taking everything available inside it, including the Claude Code credentials in ~/.claude. The Dev Containers extension in VS Code also forwards your Git credentials and SSH agent, and the reference firewall allows GitHub, so a run can push in your name. Use containers only with repositories you trust, never mount ~/.ssh or cloud key files into one, and after a run without prompts read .git/config, the list of .git/hooks and the .devcontainer diff before you rebuild or commit.

That last check matters for the same reason the [hooks post](https://zero2vibecode.com/blog/claude-code-hooks-pretooluse) tells you to read a repository's hooks before trusting it: code that runs automatically runs with whatever rights it was given.

## Cleaning up

The volume outlives the container, and so does the login inside it. When you are finished, run `claude auth logout` inside the container, then delete the container and the volumes whose names start with `claude-code-config-` and `claude-code-bashhistory-` (`docker volume ls`, then `docker volume rm`). Delete a codespace separately: even stopped, it still takes up space.

## Frequently asked questions

**Does the :1.0 tag pin the Claude Code version?**

No. It pins the feature's install script. The feature installs the latest Claude Code, which then updates itself. To pin a version, use the reference container's CLAUDE_CODE_VERSION build argument and turn off the autoupdater.

**Why does Claude Code ask me to sign in again after a rebuild?**

A rebuild discards the home folder, and ~/.claude with it. Mount a named volume at ~/.claude and point CLAUDE_CONFIG_DIR to the same path.

**Can I run --dangerously-skip-permissions as root?**

No. As root, the CLI does not accept that flag. The reference container runs as the regular user node.

## Go deeper

The [dev container lesson](https://zero2vibecode.com/learn/claude-code-pro/6-1-dev-containers) has a practice terminal where you write a `devcontainer.json` with the feature, a named volume and a matching `CLAUDE_CONFIG_DIR`. It is the quickest way to get the second layer right before your first rebuild wipes a login.
