# Claude Code in a dev container

Course: Claude Code: Pro Level — https://zero2vibecode.com/learn/claude-code-pro
Canonical URL: https://zero2vibecode.com/learn/claude-code-pro/6-1-dev-containers
Verified against: Claude Code v2.1.292 (2026-10-15)
Last updated: 2026-10-15

A dev container holds the whole Claude Code process inside, not just shell commands. The feature adds the CLI to any container, a named volume keeps your login across rebuilds, and the reference container with a firewall and a regular user is the place where running without prompts is acceptable.

In the sandbox lesson, Claude Code restricted **shell commands**. A dev container goes further: it is a Docker container your editor connects to — on your computer or in a cloud service such as GitHub Codespaces. The **whole** Claude Code process lives inside it: the file-editing tools, MCP servers, hooks. Commands run in the container, and edits reach your repository through a mount.

## Three layers

Each layer builds on the one before:

| 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

The feature is added with one line in `.devcontainer/devcontainer.json`:

```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. `postCreateCommand` adds `bubblewrap` and `socat` — without them, the Bash sandbox does not work on Linux.

## Layer 2: a login that stays

Log 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 is the same: after **Rebuild Container**, `/workspaces` is still there, but the login is gone.

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

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

If the login still disappears, check that the volume's `target` and `CLAUDE_CONFIG_DIR` name **the same** path.

## Layer 3: the reference container

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

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

This is the container the documentation supports for runs with `--dangerously-skip-permissions`. As root, the CLI does not accept that flag at all.

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 must include `claude.ai` and `platform.claude.com`, or logging in and refreshing the login will hit the firewall. If you need PyPI or a private package registry, add those to the list too and rebuild.
3. **Pin the version.** In the `CLAUDE_CODE_VERSION` build argument, replace `latest` with a specific version, and add `"DISABLE_AUTOUPDATER": "1"` to `containerEnv`.

Checking the firewall is simple: inside the container, a request to `example.com` should fail, and one to `api.github.com` should go through.

When everything checks out, one unattended run inside the container — with the same limits as in the level on running autonomously:

```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
```

## Cleaning up

The volume outlives the container — and so does your login in it. When you are done, 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.

**Careful.** 
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 into the container, and the reference firewall allows GitHub — so a run can push in your name. Use a container only with repositories you trust, and never mount ~/.ssh or files with cloud keys into it. After a run without prompts, read .git/config, the list of .git/hooks and the .devcontainer diff — before rebuilding and before git commit.

**Practice in the terminal.** Create the .devcontainer folder and write into devcontainer.json a base image, the Claude Code feature with the :1.0 tag, a named volume with target=/home/vscode/.claude, and containerEnv where CLAUDE_CONFIG_DIR points to the same path. In the Ubuntu base image the user is vscode, which is why the path uses it.
