Claude Code Dev Containers: Isolation That Holds

Add the Claude Code Dev Container Feature to the features block of your .devcontainer/devcontainer.json and rebuild. It works with anything supporting the Dev Containers spec, and in VS Code or Codespaces it also adds the Claude Code extension. If the build fails on Node, add the Node feature above the Claude Code one.
Mounting a volume at ~/.claude is not enough on its own. Claude Code stores your OAuth account, personal MCP servers and per-project trust in ~/.claude.json, a separate file outside that directory. Mount the volume and also set CLAUDE_CONFIG_DIR to the same path so that file lands inside the volume too.
Safer than on the host, but not safe in general. The container confines command execution and runs as a non-root user, but a bypassed session can still exfiltrate anything accessible inside the container, including Claude Code's own credentials, and can modify any file in the bind-mounted workspace, which appears on your host. Use it only with trusted repositories.
It can set defaults, not enforce rules. Claude Code reads /etc/claude-code/managed-settings.json at the highest precedence, but the Dockerfile that copies it in lives in the repository, so anyone with write access can remove that step. For policy engineers cannot bypass, use server-managed settings or your MDM instead.
The feature always installs the latest release and the CLI auto-updates itself, and the version tag on the feature pins its install script rather than the CLI. To pin properly, install a fixed version from your Dockerfile instead of using the feature, and set DISABLE_AUTOUPDATER in containerEnv.

Key Takeaway
Running Claude Code in a dev container means the commands it runs execute inside the container rather than on the host, while edits to project files appear in your local repository. Installation is one entry in the features block of devcontainer.json. The isolation is real but not absolute — it does not protect the credentials stored inside the container.
I rebuilt a container three times in one afternoon and signed in to Claude Code three times, which is exactly two times more than anyone should tolerate. I had already mounted a volume at the config directory, which everybody tells you to do, and it had not helped at all. The reason is a single file that lives next to that directory rather than inside it.
Dev containers are the most straightforward way to give an agent a bounded environment, and most of the guidance stops at the install step. This post goes past it: the auth that actually survives a rebuild, how to apply organisation policy inside the image and why the obvious way is bypassable, the egress firewall, and an honest account of what the isolation does and does not cover.
Claude Code goes into any dev container through a published feature. The configuration works with anything supporting the Dev Containers spec — VS Code, GitHub Codespaces, JetBrains IDEs, Cursor — and when you open the container in VS Code or Codespaces it also adds the Claude Code extension, which other editors ignore.
// .devcontainer/devcontainer.json — the whole install.
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
}
}
// The :1.0 tag pins the FEATURE'S INSTALL SCRIPT, not the Claude
// Code release. The feature installs the latest CLI, and the CLI
// auto-updates itself inside the container by default.
// If the build stops with "Failed to install Node.js and npm",
// add the Node feature ABOVE the Claude Code one and rebuild:
// "ghcr.io/devcontainers/features/node:1": {}Once it is running, both the extension panel and the terminal command run inside the container and share the same configuration directory, so there is no split-brain between the two ways of starting a session. The version tag on the feature pins its install script rather than the CLI release, which matters for reproducible builds and is covered further down.
By default the container's home directory is discarded on rebuild, so engineers sign in again every time. Mounting a volume at the config directory is the obvious fix and it is not sufficient on its own, because Claude Code stores your OAuth account, personal MCP servers and per-project trust in a separate file that sits outside that directory. Mount the volume and point the config-directory variable at the same path, so that file lands inside the volume too.
// Auth does NOT survive a rebuild by default, and mounting a
// volume at ~/.claude alone is not enough — the OAuth account,
// personal MCP servers and per-project trust live in
// ~/.claude.json, a separate file OUTSIDE that directory.
//
// Mount the volume AND point CLAUDE_CONFIG_DIR at the same path
// so Claude Code writes .claude.json inside the volume too.
"mounts": [
"source=claude-code-config,target=/home/node/.claude,type=volume"
],
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/node/.claude"
}
// Replace /home/node with your container's remoteUser home.
// To isolate state per project instead of sharing one volume
// across every repository, put the devcontainer id in the
// source name, as the reference configuration does.In GitHub Codespaces the config directory persists when you stop and start a codespace but is cleared on rebuild, so the same configuration applies. To carry authentication across codespaces entirely, store a long-lived token generated by the setup-token command, or an API key, as a Codespaces secret — Codespaces exposes secrets as environment variables inside the container automatically, so nothing needs mounting at all.
A dev container is a convenient place to apply organisation policy, because the same image runs on every engineer's machine. Claude Code reads a managed-settings file from a fixed path on Linux and applies it at the highest precedence in the settings hierarchy, above anything an engineer sets in their own config or the project directory.
# Managed settings inside the image. Claude Code reads this
# path on Linux and applies it at the HIGHEST precedence, above
# anything an engineer sets in ~/.claude or the project's .claude.
RUN mkdir -p /etc/claude-code
COPY managed-settings.json /etc/claude-code/managed-settings.json
# But the Dockerfile lives in the repository, so anyone with
# write access can change or delete that step. For policy that
# engineers cannot bypass by editing repository files, deliver
# it through server-managed settings or your MDM instead.
# To pin the CLI version for reproducible builds, skip the
# feature and install a fixed version from the Dockerfile, then:
# "containerEnv": { "DISABLE_AUTOUPDATER": "1" }The catch is worth being blunt about: the Dockerfile lives in the repository, so anyone with write access can change or remove the step that copies that file in. It is a good way to make the default configuration right, and a poor way to make a rule unbreakable. For policy engineers cannot bypass by editing repository files, deliver it through server-managed settings or your device management instead, and treat the Dockerfile copy as a convenience rather than a control.
You can limit outbound traffic to only the domains Claude Code and your toolchain need. The reference container ships a firewall script that blocks all outbound traffic except an allowlist, and because running a firewall inside a container needs extra privileges, the reference grants two network capabilities through the run arguments.
Neither the script nor those capabilities are required for Claude Code itself — you can leave them out entirely and rely on your own network controls, which is the right choice if your organisation already enforces egress at a layer you trust more than a script in a repository. What matters is that something bounds egress, not that it is this particular script.
Because the container runs as a non-root user and confines command execution, it is a defensible place to run without permission prompts for unattended work. The CLI rejects the bypass flag when launched as root, so confirm the remote user is a non-root account before expecting it to work at all.
The container is not a containment boundary for your credentials. Run with permissions skipped and a malicious project can exfiltrate anything accessible inside the container, including the Claude Code credentials stored in the config directory. Claude can also modify any file in the bind-mounted workspace, which appears directly on your host. Use dev containers with trusted repositories, and avoid mounting host secrets such as SSH keys or cloud credential files.
What a dev container actually protects against:
| Concern | Protected | Why |
|---|---|---|
| Commands touching the host filesystem | Yes | Execution is confined to the container |
| Edits to your project files | No | The workspace is bind-mounted and appears on your host |
| Credentials inside the container | No | A bypassed session can reach anything in the container |
The feature always installs the latest release and the CLI auto-updates itself inside the container, which is the right default for most teams and the wrong one when you need every engineer on an identical version:
If you set the non-essential-traffic variable to opt out of telemetry, be aware it also disables the feature-flag evaluation that Remote Control and several other features depend on. Sessions in that container simply will not have them, and the failure looks like a missing feature rather than a policy choice you made. That is usually fine in a build container and surprising in a development one.
A dev container is one of several ways to bound what an agent can do, and it is the one that also solves environment consistency — the same compilers, the same versions, the same tools for everyone. That dual purpose is why it is worth the setup even for teams who are not primarily worried about isolation.
If you want fewer prompts without disabling safety checks, auto mode is the better answer than the bypass flag: it routes tool calls through a classifier rather than removing the gate. And if you want to prevent engineers from using the bypass flag at all, that is a managed-settings key, not something the container can enforce on its own.
The setup that ended my rebuild-and-sign-in loop was two lines: a named volume and a config-directory variable pointing at the same path. Everything else here is about being precise on what the container buys you. It gives every engineer the same environment and keeps command execution off the host, which is worth a great deal. It does not make an untrusted repository safe to run unattended, and no amount of firewall script changes that.
Sources & further reading