A Dev Container Feature that installs Claude Code with multi-layered security hardening for running AI coding agents in sandboxed environments.
Designed for use with --dangerously-skip-permissions (bypass mode), where the agent can execute arbitrary commands without user confirmation. The security model assumes the agent is untrusted and restricts what damage it can do.
Create .devcontainer/devcontainer.json in your project:
Open in VS Code with the Dev Containers extension, or use the devcontainer CLI:
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . claude --dangerously-skip-permissionsVS Code Dev Containers are not a security boundary. By Microsoft's own design, the VS Code server running inside the container is in the same trust boundary as the VS Code client on the host. Specifically:
-
IPC Socket Escape: VS Code injects Unix sockets (
/tmp/vscode-ipc-*.sock) and environment variables (VSCODE_IPC_HOOK_CLI) into the container. Any process can use these to execute arbitrary commands on the host via VS Code'sTerminalServiceAPI. -
Credential Forwarding: VS Code forwards Git credentials, SSH agents, and GPG keys into the container, giving the agent access to your identity.
-
devcontainer.json Manipulation: An agent can modify
.devcontainer/devcontainer.jsonto add host filesystem mounts orinitializeCommandentries that execute on the host at next rebuild. -
Network Exfiltration: By default, containers have full outbound network access. An agent can exfiltrate code, secrets, or data to any endpoint.
This Feature applies defense-in-depth mitigations across all four vectors.
An iptables-based allowlist firewall configured at container start:
- Default-deny outbound: All traffic blocked except explicitly allowed domains
- Allowlist: Only Anthropic API (
api.anthropic.com,claude.ai,console.anthropic.com,statsig.anthropic.com) and VS Code marketplace infrastructure - IPv6 disabled: Prevents bypass through IPv6 stack
- DNS restricted: Only the container's configured resolver is allowed, blocking DNS tunneling
- Host gateway: Only the Docker host gateway IP is permitted (not the entire subnet)
- Git push blocked: GitHub/GitLab are not on the firewall allowlist, so
git pushfails at the network level
Prevents the agent from using your identity:
containerEnvclearsSSH_AUTH_SOCK,GIT_ASKPASS,GH_TOKEN,GITHUB_TOKEN, and VS Code's git askpass helpers at the container levelGIT_CONFIG_VALUE_0setscredential.helperto/bin/false- VS Code settings disable
github.gitAuthenticationandgit.terminalAuthentication - Named volumes persist Claude's own auth across rebuilds without exposing host credentials
Multiple overlapping mitigations for the IPC socket escape vector:
| Mitigation | What It Does | Coverage |
|---|---|---|
remoteEnv (null) |
Tells VS Code not to set VSCODE_IPC_HOOK_CLI and related variables |
VS Code-spawned processes |
harden-env.sh in .bashrc |
Unsets IPC variables at shell startup, before the interactive guard | All bash sessions including non-interactive |
| Socket cleanup (immediate) | Deletes /tmp/vscode-*.sock files at firewall init |
Sockets created before postStartCommand |
| Socket cleanup (daemon) | Background process deletes sockets every 30s for ~5 minutes | Late-created sockets (VS Code creates some 60s+ after attach) |
devcontainer.json immutable |
chattr +i prevents modification of devcontainer config files |
Blocks host mount injection and malicious initializeCommand |
--cap-drop=ALLinrunArgsdrops all Linux capabilities (the Feature adds back onlyNET_ADMIN+NET_RAWfor firewall setup)- SUID/SGID bits stripped from all binaries except
sudo - Sudo access limited to a single command (
init-firewall.sh), then the user is removed from the sudo group entirely - Firewall script, hardening script, and devcontainer.json are made immutable with
chattr +i
| Mode | IPC Escape Risk | Setup Complexity | IDE Experience |
|---|---|---|---|
| Standalone Docker (Recommended) | None — no VS Code in container | Low | VS Code on host |
| VS Code Desktop | Best-effort mitigations (small race window) | Lowest | Full VS Code |
| Browser (code-server) | None — no host IPC bridge | Medium | VS Code in browser |
| Headless CLI | None — no IDE at all | Medium | Terminal only |
| Plain Docker | None — no devcontainer tooling | Highest | Bring your own |
Run Claude in an isolated Docker container with no VS Code processes inside. You edit files with VS Code on the host normally — Claude sees the same files via a bind mount. No IPC sockets, no escape risk.
Build:
docker build -t claude-code-sandbox -f standalone/Dockerfile .Run (Linux/macOS):
docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
claude-code-sandboxRun (Windows PowerShell):
docker run -it --rm `
--cap-add=NET_ADMIN --cap-add=NET_RAW `
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" `
-v claude-code-data:/home/vscode/.local/share/claude `
-v "${PWD}:/workspaces/project" `
claude-code-sandboxRun (Windows CMD):
docker run -it --rm ^
--cap-add=NET_ADMIN --cap-add=NET_RAW ^
-v "%USERPROFILE%\claude-sandbox-config:/home/vscode/.claude" ^
-v claude-code-data:/home/vscode/.local/share/claude ^
-v "%cd%:/workspaces/project" ^
claude-code-sandboxShell alias (Linux/macOS):
alias claude-sandbox='docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
claude-code-sandbox'PowerShell function (Windows):
function claude-sandbox {
docker run -it --rm `
--cap-add=NET_ADMIN --cap-add=NET_RAW `
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" `
-v claude-code-data:/home/vscode/.local/share/claude `
-v "${PWD}:/workspaces/project" `
claude-code-sandbox @Args
}Workflow: Open your project in VS Code on the host normally. In a terminal, cd to the project and run claude-sandbox. Claude edits the same files via bind mount — VS Code sees changes in real-time. No VS Code processes run inside the container.
Examples:
# Interactive session (default)
claude-sandbox
# One-shot prompt
docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$(pwd):/workspaces/project" \
claude-code-sandbox claude -p "Fix the bug"
# Just a shell
docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$(pwd):/workspaces/project" \
claude-code-sandbox bash
# Pass API key explicitly
docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-e ANTHROPIC_API_KEY \
-v "$(pwd):/workspaces/project" \
claude-code-sandbox
# Max hardening: drop all capabilities first
docker run -it --rm \
--cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$(pwd):/workspaces/project" \
claude-code-sandboxGit Identity:
The container has no git identity configured by default. Bind-mount your host's git config so the agent can create commits with your name and email:
# Find your git config location:
git config --global --list --show-origin | head -1
# Common locations: ~/.gitconfig or ~/.config/git/config
# Linux/macOS — mount whichever file your system uses:
docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$HOME/.config/git/config:/home/vscode/.gitconfig:ro" \
-v "$(pwd):/workspaces/project" \
claude-code-sandboxImportant: The source file must exist on the host, otherwise Docker creates an empty directory at that path which breaks git. Verify with ls -la ~/.gitconfig or ls -la ~/.config/git/config before using.
This is safe — user.name and user.email are commit metadata only, not credentials. The credential helper is overridden to /bin/false via environment variables, GitHub is blocked by the firewall, and the pre-push hook rejects all pushes.
Host Config Folder:
Instead of using a named Docker volume, mount a dedicated host folder as ~/.claude/ inside the container. This gives you full control over Claude Code's configuration — CLAUDE.md instructions, custom commands, settings, and more — while keeping auth credentials in one place.
Initial setup:
# Create a config folder (one per language/use-case, or a shared one)
mkdir -p ~/claude-sandbox-config/commands
# Copy your auth credentials (OAuth tokens — required for Claude to authenticate)
cp ~/.claude/.credentials.json ~/claude-sandbox-config/
# Add global instructions
cat > ~/claude-sandbox-config/CLAUDE.md << 'EOF'
# My Sandbox Instructions
Add your language-specific rules and conventions here.
EOFThe folder is mounted read-write because Claude Code writes runtime data (history, sessions, stats) during use. Your instructions and credentials persist across container restarts. Project-level CLAUDE.md files come in automatically through the project bind mount.
Per-language config folders:
Create separate config folders for different languages or use-cases, each with their own CLAUDE.md and custom commands:
# ~/claude-al-development/ — AL / Business Central
# ~/claude-python/ — Python projects
# ~/claude-typescript/ — TypeScript projects
alias claude-al='docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-al-development:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
claude-code-sandbox'
alias claude-python='docker run -it --rm \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-python:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
claude-code-sandbox'What to put in the config folder:
| File/Directory | Purpose |
|---|---|
.credentials.json |
OAuth tokens (copy from ~/.claude/.credentials.json) |
CLAUDE.md |
Global instructions loaded by Claude Code |
commands/ |
Custom slash commands (.md files) |
settings.json |
Claude Code settings (permissions, model, etc.) |
settings.local.json |
Local permission overrides |
Runtime files like history.jsonl, sessions/, debug/, and statsig/ will be created automatically in the folder during use.
The standard Dev Containers workflow: open the folder in VS Code, click "Reopen in Container."
Trade-off: VS Code's remote architecture injects IPC sockets and environment variables into the container by design. The mitigations above are best-effort — there is a small race window between socket creation and cleanup, and VS Code may re-inject environment variables in ways that bypass the shell hardening.
This mode is suitable for working on code you broadly trust, with the firewall preventing network exfiltration as the primary security boundary.
Run a VS Code-compatible editor inside the container and connect via browser. No VS Code desktop client means no IPC socket bridge to the host — the container is genuinely isolated.
With the devcontainer CLI:
// .devcontainer/devcontainer.json — add to the Quick Start config:
{
// ... existing config ...
"forwardPorts": [8080],
"postCreateCommand": "curl -fsSL https://code-server.dev/install.sh | sh && echo '{\"hasCompletedOnboarding\":true,\"numStartups\":1,\"installMethod\":\"native\"}' > ~/.claude.json"
}devcontainer up --workspace-folder .
# Open http://localhost:8080 in your browserWith plain Docker (no devcontainer CLI needed):
# Build the container image with the Feature pre-applied
devcontainer build --workspace-folder . --image-name claude-sandbox
# Or if you don't want devcontainer CLI at all, build from a Dockerfile:
docker run -it --name claude-sandbox \
--cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW \
-p 8080:8080 \
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
-e GIT_TERMINAL_PROMPT=0 \
-e SSH_AUTH_SOCK= \
-e GIT_ASKPASS=/bin/false \
-e GH_TOKEN= \
-e GITHUB_TOKEN= \
mcr.microsoft.com/devcontainers/base:ubuntu-24.04 \
bashNote: When using plain docker run, the Feature's install script doesn't run automatically. You would need to either:
- Use
devcontainer buildfirst to bake the Feature into an image, thendocker runthat image - Or install Claude Code and the firewall manually inside the container
Option 1 is recommended — it gives you a plain Docker image with all hardening pre-applied:
# One-time: build the image using devcontainer CLI
devcontainer build --workspace-folder . --image-name claude-sandbox
# Then use plain Docker from here on — no devcontainer CLI needed
docker run -it --rm \
--cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
-e GIT_TERMINAL_PROMPT=0 \
-e SSH_AUTH_SOCK= \
-e GIT_ASKPASS=/bin/false \
-e GH_TOKEN= \
-e GITHUB_TOKEN= \
-u vscode \
claude-sandbox \
bash -c "sudo /usr/local/bin/init-firewall.sh && code-server --bind-addr 0.0.0.0:8080 /workspaces/project"
# Open http://localhost:8080Run Claude Code directly without any IDE:
# With devcontainer CLI
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . claude --dangerously-skip-permissions -p "Fix the bug in auth.ts"
# With plain Docker (using pre-built image from above)
docker run -it --rm \
--cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$HOME/claude-sandbox-config:/home/vscode/.claude" \
-v claude-code-data:/home/vscode/.local/share/claude \
-v "$(pwd):/workspaces/project" \
-e GIT_TERMINAL_PROMPT=0 \
-e SSH_AUTH_SOCK= \
-e GIT_ASKPASS=/bin/false \
-e GH_TOKEN= \
-e GITHUB_TOKEN= \
-u vscode \
claude-sandbox \
bash -c "sudo /usr/local/bin/init-firewall.sh && claude --dangerously-skip-permissions -p 'Fix the bug in auth.ts'"No IPC sockets exist in either approach. Combined with the network firewall, this provides strong isolation.
-
IPC race window (VS Code Desktop): There is a brief window between VS Code creating IPC sockets and the cleanup daemon deleting them. An agent that acts in this window could potentially use a socket before it is removed.
-
chattron overlay filesystems: Thechattr +iimmutability flag may not work on Docker's default overlay2 filesystem. The scripts log warnings when this fails. The security value is defense-in-depth; the firewall is the primary boundary. -
DNS is point-in-time: Domain allowlist IPs are resolved once at container start. If a service's IP changes during a long session, connections may break. Restart the container to re-resolve.
-
VS Code may change behavior: Microsoft could change how IPC sockets or environment variables work in future VS Code versions, potentially bypassing current mitigations. The layered approach reduces the impact of any single mitigation being defeated.
-
remoteEnvnull behavior: Setting variables tonullinremoteEnvtells VS Code not to set them, but this depends on VS Code respecting the directive. It is not equivalent tounsetat the OS level (whichharden-env.shprovides).
| Option | Type | Default | Description |
|---|---|---|---|
version |
string | latest |
Claude Code version to install (latest, stable, or specific like 1.0.58) |
Thanks to Janrik Ö. for flagging the VS Code container escape issue — without his heads-up this would likely have gone unaddressed.
The IPC escape mitigations are based on research and techniques from:
- Daniel Demmel — Comprehensive hardening guide for dev containers running AI agents
- The Red Guild — Technical breakdown of VS Code container escape mechanisms
- Anthropic — Official Claude Code devcontainer reference
MIT
Last tested: 2026-03-04
{ "name": "Claude Code", "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/stefanmaron/claudeCodeAlDevContainer/claude-code:latest": {} }, "runArgs": ["--cap-drop=ALL"], "remoteUser": "vscode", "mounts": [ "source=claude-code-config,target=/home/vscode/.claude,type=volume", "source=claude-code-data,target=/home/vscode/.local/share/claude,type=volume" ], "containerEnv": { "GIT_TERMINAL_PROMPT": "0", "SSH_AUTH_SOCK": "", "GIT_ASKPASS": "/bin/false", "VSCODE_GIT_ASKPASS_MAIN": "", "VSCODE_GIT_ASKPASS_NODE": "", "VSCODE_GIT_ASKPASS_EXTRA_ARGS": "", "GH_TOKEN": "", "GITHUB_TOKEN": "", "GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "credential.helper", "GIT_CONFIG_VALUE_0": "/bin/false" }, "remoteEnv": { "VSCODE_IPC_HOOK_CLI": null, "VSCODE_GIT_IPC_HANDLE": null, "REMOTE_CONTAINERS_IPC": null, "REMOTE_CONTAINERS_SOCKETS": null, "REMOTE_CONTAINERS_DISPLAY_SOCK": null, "GPG_AGENT_INFO": "", "BROWSER": "", "WAYLAND_DISPLAY": "" }, "customizations": { "vscode": { "settings": { "github.gitAuthentication": false, "git.terminalAuthentication": false } } }, "postCreateCommand": "echo '{\"hasCompletedOnboarding\":true,\"numStartups\":1,\"installMethod\":\"native\"}' > ~/.claude.json", "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", "waitFor": "postStartCommand" }