Dev container setup for Claude Code
PLA 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.jsonthat 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-permissionsis 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.jsonline 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.
| Situation | Use | Why |
|---|---|---|
| Solo developer, trusted repository, interactive sessions | The Bash sandbox (/sandbox) on the host | No Docker overhead; it stops most prompts for shell commands |
| Team that needs identical toolchains and faster onboarding | Dev container with the Claude Code feature | One file defines the environment; commands run inside the container, not on the laptop |
| Unattended runs with fewer or no permission prompts | Dev container + egress firewall + non-root user | Limits what a bypassed session can reach and modify |
| One full stack (database, services, seed data) per agent task | Ephemeral environments | A dev container is one long-lived environment per developer, not one per task |
| Hosted, disposable sandboxes (E2B, Daytona, container-use) | Agent sandboxes compared | A 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).
-
Save this as
.devcontainer/devcontainer.jsonin the repository root, or merge thefeatures,mounts, andcontainerEnvblocks 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.0tag tracks the feature’s 1.0.x install script (use:1.0.5for an exact pin), not the Claude Code release. The feature installs the latest Claude Code, which then auto-updates inside the container. -
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 . -
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. -
Confirm the basics from the same terminal:
claude --versionprints a version,node --versionprintsv22or later, andwhoamiprintsnode, notroot.
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/DockerfileFROM 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-codeCOPY managed-settings.json /etc/claude-code/managed-settings.jsonThe 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:
- 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. - 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:
~/.claudepersists across stopping and starting a codespace but is cleared on rebuild, so keep the volume andCLAUDE_CONFIG_DIRconfiguration.- To stay signed in across codespaces, store
ANTHROPIC_API_KEY, or aCLAUDE_CODE_OAUTH_TOKENgenerated withclaude 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.shRUN chmod +x /usr/local/bin/init-firewall.shThen 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.
postStartCommandre-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.combut notclaude.aiorplatform.claude.com, which browser sign-in and OAuth token refresh use according to Anthropic’s network access requirements. Addclaude.aiandplatform.claude.comto thefor domain inlist (andmcp-proxy.anthropic.comif you use claude.ai connectors), or authenticate withCLAUDE_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 -pstill 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 (stableis 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.disableBypassPermissionsModeto"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:
name: devcontaineron: pull_request: paths: ['.devcontainer/**', '.mcp.json', '.claude/**']permissions: contents: readjobs: 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 regressionset -euo pipefailtest "$(whoami)" != "root" # --dangerously-skip-permissions needs non-rootnode -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 holdstest "$CLAUDE_CONFIG_DIR" = "$HOME/.claude" # sign-in survives rebuildstest -f /etc/claude-code/managed-settings.json # team policy is in the imageif curl -s --max-time 5 https://example.com >/dev/null; then echo "egress not blocked"; exit 1ficurl -s --max-time 5 -o /dev/null https://api.anthropic.com # the model API is reachablenpm test # the project's own suite runs in the containerDrop 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.
When the Claude Code dev container breaks
Section titled “When the Claude Code dev container breaks”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”- Claude Code for teams — the section overview and the order to roll things out.
- Enterprise integration for Claude Code — managed settings that engineers cannot edit away.
- Proxy configuration for Claude Code — route container traffic through a corporate proxy.
- Team collaboration with Claude Code — shared
CLAUDE.md, skills, and review rules that the container carries. - Claude Code in CI/CD — the headless runs that often reuse this container image.