Skip to content

Repository files navigation

Claude Code Dev Container Feature

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.

Quick Start

Create .devcontainer/devcontainer.json in your project:

{
    "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"
}

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

Security Model

The Problem

VS 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:

  1. 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's TerminalService API.

  2. Credential Forwarding: VS Code forwards Git credentials, SSH agents, and GPG keys into the container, giving the agent access to your identity.

  3. devcontainer.json Manipulation: An agent can modify .devcontainer/devcontainer.json to add host filesystem mounts or initializeCommand entries that execute on the host at next rebuild.

  4. 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.

Layer 1: Network Firewall

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 push fails at the network level

Layer 2: Credential Stripping

Prevents the agent from using your identity:

  • containerEnv clears SSH_AUTH_SOCK, GIT_ASKPASS, GH_TOKEN, GITHUB_TOKEN, and VS Code's git askpass helpers at the container level
  • GIT_CONFIG_VALUE_0 sets credential.helper to /bin/false
  • VS Code settings disable github.gitAuthentication and git.terminalAuthentication
  • Named volumes persist Claude's own auth across rebuilds without exposing host credentials

Layer 3: VS Code IPC Escape Prevention

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

Layer 4: Privilege Reduction

  • --cap-drop=ALL in runArgs drops all Linux capabilities (the Feature adds back only NET_ADMIN + NET_RAW for 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

Usage Modes

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

Standalone Docker (Recommended)

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

Run (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-sandbox

Run (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-sandbox

Shell 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-sandbox

Git 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-sandbox

Important: 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.
EOF

The 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.

VS Code Desktop (Convenience)

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.

Browser-Based (Full Isolation)

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 browser

With 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 \
    bash

Note: When using plain docker run, the Feature's install script doesn't run automatically. You would need to either:

  1. Use devcontainer build first to bake the Feature into an image, then docker run that image
  2. 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:8080

Headless CLI (No IDE)

Run 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.

Known Limitations

  • 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.

  • chattr on overlay filesystems: The chattr +i immutability 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.

  • remoteEnv null behavior: Setting variables to null in remoteEnv tells VS Code not to set them, but this depends on VS Code respecting the directive. It is not equivalent to unset at the OS level (which harden-env.sh provides).

Options

Option Type Default Description
version string latest Claude Code version to install (latest, stable, or specific like 1.0.58)

Credits

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

License

MIT


Last tested: 2026-03-04

About

Dev Container Feature for installing Claude Code CLI

Resources

Stars

23 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages