Skip to content

A Claude Code dev container is a .devcontainer/devcontainer.json checked into the repository that installs Claude Code through Anthropic’s Dev Container Feature, so every command Claude runs executes in an identical, isolated environment instead of on the host. A team-grade setup adds a named volume that keeps sign-in across rebuilds, a managed policy file, an egress firewall, and a CI smoke test.

A new engineer joins on Monday and loses two days to Node versions, a local PostgreSQL, and a proxy variable that nobody wrote down. Meanwhile, a teammate runs Claude Code unattended on a laptop that holds SSH keys and production cloud credentials. One dev container fixes both problems, but only if it is built correctly: many copy-paste configs install Claude Code on an unsupported Node version, bind-mount host credentials into the container, and trust a proxy variable to isolate the network.

This page is for the developer who sets up the container and the tech lead who owns it for the team. It covers running Claude Code inside a container. Writing Dockerfiles and Kubernetes manifests with Claude Code is on Docker and Kubernetes setup with Claude Code, and the cross-tool comparison of permission modes and sandboxes is permissions, sandboxes and approval modes.

What a team dev container for Claude Code gives you

Section titled “What a team dev container for Claude Code gives you”
  • A devcontainer.json that installs Claude Code with the official feature and survives rebuilds without a new sign-in.
  • A pinned-version Dockerfile variant with organization policy in /etc/claude-code/managed-settings.json.
  • A default-deny egress firewall, and a clear view of the gaps in Anthropic’s reference script.
  • Rules for when --dangerously-skip-permissions is acceptable, and when auto mode or the Bash sandbox is the better choice.
  • A CI job that proves the container is correct on every change, so nobody reviews devcontainer.json line by line.
  • Three copy-paste prompts: scaffold, audit, and verify.

Which isolation do you need: dev container, Bash sandbox, or both?

Section titled “Which isolation do you need: dev container, Bash sandbox, or both?”

Claude Code has two isolation layers that people confuse. The Bash sandbox is built into Claude Code and restricts each shell command with OS primitives (Seatbelt on macOS, bubblewrap on Linux and WSL2). A dev container isolates the whole toolchain, including Claude Code itself, from your host.

SituationUseWhy
Solo developer, trusted repository, interactive sessionsThe Bash sandbox (/sandbox) on the hostNo Docker overhead; it stops most prompts for shell commands
Team that needs identical toolchains and faster onboardingDev container with the Claude Code featureOne file defines the environment; commands run inside the container, not on the laptop
Unattended runs with fewer or no permission promptsDev container + egress firewall + non-root userLimits what a bypassed session can reach and modify
One full stack (database, services, seed data) per agent taskEphemeral environmentsA dev container is one long-lived environment per developer, not one per task
Hosted, disposable sandboxes (E2B, Daytona, container-use)Agent sandboxes comparedA tool catalog, outside this page’s scope

The layers compose. Inside a dev container, the Bash sandbox still adds a per-command boundary, at a cost covered in the failure modes below.

Add Claude Code to devcontainer.json with the official feature

Section titled “Add Claude Code to devcontainer.json with the official feature”

Anthropic publishes a Claude Code Dev Container Feature. It installs the CLI, installs Node.js when the base image lacks it, and adds the Claude Code VS Code extension when you open the container in VS Code or Codespaces. Use it instead of an npm install -g in postStartCommand, which reinstalls on every start. Start from an image that already ships Node 22 or later, because the npm package requires it (engines: node >=22.0.0 for @anthropic-ai/claude-code 2.1.283, checked 2026-09-26). On an older Node, npm only prints an EBADENGINE warning and installs a CLI that your runtime does not support. The feature’s own fallback is no safer: on a Debian/Ubuntu or yum-based image without Node, its install.sh adds the NodeSource 18.x repository, and other distros get whatever nodejs their package manager ships (checked on the main branch, feature version 1.0.5, 2026-09-26).

  1. Save this as .devcontainer/devcontainer.json in the repository root, or merge the features, mounts, and containerEnv blocks into your existing file:

    {
    "name": "app + Claude Code",
    "image": "mcr.microsoft.com/devcontainers/typescript-node:22",
    "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
    },
    "remoteUser": "node",
    "mounts": [
    "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume"
    ],
    "containerEnv": {
    "CLAUDE_CONFIG_DIR": "/home/node/.claude"
    },
    "postCreateCommand": "npm ci",
    "forwardPorts": [3000]
    }

    The :1.0 tag tracks the feature’s 1.0.x install script (use :1.0.5 for an exact pin), not the Claude Code release. The feature installs the latest Claude Code, which then auto-updates inside the container.

  2. Rebuild the container. In VS Code or Cursor, open the Command Palette and run Dev Containers: Rebuild Container. From a terminal, use the Dev Containers CLI: npx @devcontainers/cli up --workspace-folder .

  3. Open a terminal inside the container and run claude. With an Anthropic account, sign in through the browser. With Amazon Bedrock, Google Cloud, or Microsoft Foundry, pass the cloud credentials as environment variables (containerEnv, a Codespaces secret, or workload identity), not as mounted credential files. The provider setup is on Claude Code authentication.

  4. Confirm the basics from the same terminal: claude --version prints a version, node --version prints v22 or later, and whoami prints node, not root.

Why a named volume and not a bind mount of ~/.claude?

Section titled “Why a named volume and not a bind mount of ~/.claude?”

Many examples bind-mount the host’s ~/.claude into the container. Do not. That directory holds your credentials and your session history for every project, and a container session with skipped permissions can read and exfiltrate anything inside the container. Anthropic’s own dev container guide warns against mounting host secrets such as ~/.ssh or cloud credential files for the same reason.

The named volume in step 1 keeps sign-in and user settings across rebuilds without touching the host. CLAUDE_CONFIG_DIR matters because Claude Code keeps the OAuth account, personal MCP servers, and per-project trust in ~/.claude.json, a file outside ~/.claude. Without the variable, the volume persists but you are signed out after every rebuild. The ${devcontainerId} suffix gives each repository its own volume, so one project’s state never leaks into another.

Pin the Claude Code version and enforce team policy

Section titled “Pin the Claude Code version and enforce team policy”

The feature always installs the latest release. When a tech lead needs reproducible builds, or needs policy that a project .claude/settings.json cannot override, switch to a Dockerfile.

# .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/typescript-node:22
# Pin the CLI. 2.1.274 was the npm "stable" dist-tag on 2026-09-26.
RUN npm install -g @anthropic-ai/claude-code@2.1.274
# Organization policy: highest precedence in the settings hierarchy on Linux.
RUN mkdir -p /etc/claude-code
COPY managed-settings.json /etc/claude-code/managed-settings.json

The policy file sits next to it as .devcontainer/managed-settings.json:

{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
}
}

Point devcontainer.json at the Dockerfile with "build": { "dockerfile": "Dockerfile" } instead of image and features, add "customizations": { "vscode": { "extensions": ["anthropic.claude-code"] } } to keep the VS Code extension the feature used to add, and add "DISABLE_AUTOUPDATER": "1" to containerEnv so the pin holds. For telemetry-restricted environments, "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" also turns off non-essential traffic. It also turns off the feature-flag evaluation that Remote Control depends on, so Remote Control is unavailable in the container. If you need Remote Control, set only DISABLE_TELEMETRY instead (v2.1.283+), which keeps it available unless your organization requires Trusted Devices.

Share project config and MCP servers through the repository

Section titled “Share project config and MCP servers through the repository”

Everything project-scoped already travels with the checkout: CLAUDE.md, .claude/settings.json, .claude/skills/, .claude/agents/, and .mcp.json. Define team MCP servers at project scope in .mcp.json (see MCP setup for Claude Code), then do two more things the checkout cannot do for you:

  1. Install the binaries that stdio servers need (uv, a database client) in the Dockerfile, so the first MCP call does not fail on a missing executable.
  2. Add each remote MCP server’s domain to the firewall allowlist below.

Use the same container in GitHub Codespaces

Section titled “Use the same container in GitHub Codespaces”

Codespaces runs the same devcontainer.json in the cloud. Two details differ from a local container:

  • ~/.claude persists across stopping and starting a codespace but is cleared on rebuild, so keep the volume and CLAUDE_CONFIG_DIR configuration.
  • To stay signed in across codespaces, store ANTHROPIC_API_KEY, or a CLAUDE_CODE_OAUTH_TOKEN generated with claude setup-token (requires a Claude subscription), as a Codespaces user secret. Codespaces exposes secrets as environment variables inside the container.

Declare the secret in devcontainer.json so Codespaces prompts for it when someone creates a codespace:

{
"secrets": {
"CLAUDE_CODE_OAUTH_TOKEN": {
"description": "Long-lived Claude Code token from `claude setup-token`"
}
}
}

Never hardcode a key in devcontainer.json or pass one with ${localEnv:...} into a file you commit.

Restrict network egress with a default-deny firewall

Section titled “Restrict network egress with a default-deny firewall”

A custom Docker network plus HTTPS_PROXY does not isolate anything: nothing forces traffic through the proxy, and a direct connection still leaves the container. Use the proxy variable for routing (see proxy configuration for Claude Code) and a firewall for isolation.

Anthropic’s reference dev container ships init-firewall.sh, which sets the iptables default policy to DROP, allows GitHub’s published IP ranges and a short list of resolved domains, and then checks itself: it fails if https://example.com is reachable or https://api.github.com is not. The script needs root tools that the step-1 image does not have. Copy init-firewall.sh from the reference into .devcontainer/; this requires the Dockerfile variant, not the image-plus-feature file from step 1. Add to the Dockerfile:

RUN apt-get update && apt-get install -y --no-install-recommends \
iptables ipset dnsutils aggregate jq curl \
&& rm -rf /var/lib/apt/lists/*
COPY init-firewall.sh /usr/local/bin/init-firewall.sh
RUN chmod +x /usr/local/bin/init-firewall.sh

Then wire it in the way the reference does:

{
"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
"waitFor": "postStartCommand"
}

Do not adopt the script unchanged. Its gaps, checked against the main branch on 2026-09-26:

  • It resolves domains to IP addresses once, at start. A registry or CDN that rotates addresses breaks later in the session. postStartCommand re-runs the script on every container start, so restarting the container is the recovery.
  • Its domain list is not the full list Claude Code needs. It allows api.anthropic.com but not claude.ai or platform.claude.com, which browser sign-in and OAuth token refresh use according to Anthropic’s network access requirements. Add claude.ai and platform.claude.com to the for domain in list (and mcp-proxy.anthropic.com if you use claude.ai connectors), or authenticate with CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY.
  • It allows the host’s whole /24 network. Any service on that network, including other containers, stays reachable.
  • The reference Dockerfile starts from node:20, below the Node 22 minimum of the current npm package. Change the base image when you copy it.

Add your own package registries, your VCS host if it is not GitHub, and remote MCP server domains to the for domain in list.

Run Claude Code unattended inside the container

Section titled “Run Claude Code unattended inside the container”

The container runs Claude Code as a non-root user and confines commands to the container. That makes claude --dangerously-skip-permissions reasonable only in a container with the egress firewall above, on a trusted repository. The CLI refuses the flag when it runs as root. A bypassed session can still modify every file in the bind-mounted workspace, and those changes appear on your host immediately.

Prefer a narrower option first:

  • Auto mode keeps a classifier reviewing each action. From v2.1.283 (both release channels on 2026-10-12), it is the starting permission mode for interactive terminal and VS Code sessions on supported models. claude -p still starts in Manual (auto mode on third-party providers or with telemetry off, from v2.1.285). The pinned 2.1.274 image above predates v2.1.283, so its sessions still start in Manual until you move the pin to 2.1.283 or later (stable is 2.1.287 on 2026-10-12). For headless runs, pass --permission-mode auto, or pin an exact allowlist: claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read".
  • The Bash sandbox removes most shell prompts while keeping a per-command boundary. Enable it with /sandbox, or set "sandbox": { "enabled": true } in .claude/settings.json.
  • To forbid bypass mode for the whole team, set permissions.disableBypassPermissionsMode to "disable" in managed settings.

For the full comparison of modes across Claude Code, Codex, and Cursor, see permissions, sandboxes and approval modes.

How do you prove the dev container works without reading it?

Section titled “How do you prove the dev container works without reading it?”

A reviewer cannot tell from a diff to devcontainer.json whether sign-in still persists or whether the firewall still blocks egress. A CI job can. The devcontainers/ci action builds the container from your real config and runs a command inside it. The job below needs no secrets, because none of the checks calls the model:

.github/workflows/devcontainer.yml
name: devcontainer
on:
pull_request:
paths: ['.devcontainer/**', '.mcp.json', '.claude/**']
permissions:
contents: read
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: devcontainers/ci@v0.3
with:
push: never
runCmd: bash .devcontainer/smoke-test.sh
#!/usr/bin/env bash
# .devcontainer/smoke-test.sh: every line fails the job on a regression
set -euo pipefail
test "$(whoami)" != "root" # --dangerously-skip-permissions needs non-root
node -e 'process.exit(+process.versions.node.split(".")[0] >= 22 ? 0 : 1)' # Node 22+
claude --version | grep -q '^2\.1\.274 ' # the pin in the Dockerfile holds
test "$CLAUDE_CONFIG_DIR" = "$HOME/.claude" # sign-in survives rebuilds
test -f /etc/claude-code/managed-settings.json # team policy is in the image
if curl -s --max-time 5 https://example.com >/dev/null; then
echo "egress not blocked"; exit 1
fi
curl -s --max-time 5 -o /dev/null https://api.anthropic.com # the model API is reachable
npm test # the project's own suite runs in the container

Drop the version and managed-settings lines if you use the feature rather than the Dockerfile, and drop the egress check if you do not run the firewall. Put .devcontainer/ under CODEOWNERS so the tech lead signs off on changes; with the smoke job green, that sign-off is a check of intent, not of every line.

Use the same container with Cursor or Codex

Section titled “Use the same container with Cursor or Codex”

The container is not Claude Code-specific. Cursor supports the Dev Containers spec, so it opens the same devcontainer.json, and the Claude Code CLI works in its integrated terminal. For Codex, add npm install -g @openai/codex to the Dockerfile and add Codex’s hosts to the firewall: api.openai.com for API-key sign-in, and chatgpt.com plus auth.openai.com for ChatGPT sign-in (the default endpoints in the Codex 0.157.1 binary, checked 2026-09-26). Codex’s hosted environments are a separate system, described in Codex cloud environments.

The build stops with Failed to install Node.js and npm, or claude crashes on start. The base image has no Node, and the feature either could not install one or installed Node 18. Add "ghcr.io/devcontainers/features/node:1": { "version": "22" } above the Claude Code feature in features, rebuild, and confirm node --version prints v22 or later.

npm install -g @anthropic-ai/claude-code prints EBADENGINE. The image’s Node is older than 22. Move to a Node 22 or later image; the feature does not help here, because it keeps any Node the image already has.

Browser sign-in completes but the terminal never receives it. Editor port forwarding did not route the localhost callback. Copy the code shown in the browser and paste it at the Paste code here if prompted prompt.

You are signed out after every rebuild. The volume is mounted but CLAUDE_CONFIG_DIR is not set, so ~/.claude.json lives in the discarded home directory. Set the variable to the volume’s target path.

EACCES on ~/.claude after the first start. The volume was created by a different user. Run sudo chown -R node:node /home/node/.claude once, or delete the volume with docker volume rm and rebuild.

--dangerously-skip-permissions is refused. Claude Code is running as root. Set remoteUser to the image’s non-root user and rebuild.

Sandboxed commands fail with Can't mount proc on /newroot/proc: Operation not permitted. Bubblewrap cannot mount a fresh /proc in an unprivileged container. Set sandbox.enableWeakerNestedSandbox to true only if the container’s own firewall and user isolation are the boundary you rely on; the setting considerably weakens the inner sandbox.

npm install, a remote MCP server, or sign-in times out after the firewall starts. The destination is not on the allowlist, or its IP changed after the script resolved it. Add the domain to init-firewall.sh and restart the container.

claude --resume does not show sessions from the host. Sessions are stored per project path, and the container path (/workspaces/app) differs from the host path. This is expected. Start new sessions inside the container.

Where to go next with Claude Code for teams

Section titled “Where to go next with Claude Code for teams”