Skip to content

Instantly share code, notes, and snippets.

@YoraiLevi
Last active May 15, 2026 15:42
Show Gist options
  • Select an option

  • Save YoraiLevi/a319c347521738969ab1b39bc418134a to your computer and use it in GitHub Desktop.

Select an option

Save YoraiLevi/a319c347521738969ab1b39bc418134a to your computer and use it in GitHub Desktop.
Running Claude Code Inside Isolated Containers with Local-Feeling Persistence — a catalog of approaches, solutions, security caveats, and a recommended recipe

Complete Step-by-Step Guide: Claude Code in an Isolated Container

What you'll have when done: Claude Code running fully inside a Docker container, using --dangerously-skip-permissions freely, with your sessions and credentials persisted on disk, your project files editable as if local, and a firewall preventing any exfiltration — all without ever risking your host system, ~/.ssh, cloud credentials, or other files you didn't explicitly share.


Before You Start: Two Paths

This guide covers two ways to do this. Pick one:

Path A: Standalone Docker Path B: VS Code Devcontainer
What it is Run docker run … directly from a terminal VS Code "Reopen in Container"
Editor Any — Vim, Neovim, Helix, or no editor VS Code, Cursor, or JetBrains
Security Slightly better (no VS Code IPC socket) Good enough for most use cases
Setup effort Lower Slightly higher
Recommended for Automation, security-sensitive work Daily VS Code workflow

You can set up both from the same files. Path A is documented first, then Path B is an addendum.


Prerequisites

1. Install Docker

Windows:

  • Download and install Docker Desktop for Windows
  • During setup, enable WSL 2 backend when prompted
  • After install, open Docker Desktop and wait for the whale icon in the taskbar to turn solid (not animating)
  • Open PowerShell and verify:
    docker run --rm hello-world
    You should see "Hello from Docker!"

macOS:

  • Download Docker Desktop for Mac
  • Open it, wait for the menubar whale to stop animating
  • Verify in Terminal: docker run --rm hello-world

Linux:

  • Install Docker Engine: sudo apt-get install docker.io (Ubuntu/Debian)
  • Add yourself to the docker group: sudo usermod -aG docker $USER
  • Log out and back in, then verify: docker run --rm hello-world

2. Have Claude Code installed on the host (just for one step)

You need Claude Code on your host machine once to generate a long-lived auth token. If you don't have it yet:

npm install -g @anthropic-ai/claude-code

(Requires Node.js 18+. After the token is generated you don't need Claude Code on the host anymore if you prefer.)

3. Have a Claude subscription or API key

  • Claude Pro / Max / Teams / Enterprise subscription → you'll use the setup-token method (recommended, no usage charges beyond subscription)
  • Anthropic API key from console.anthropic.com → you'll use ANTHROPIC_API_KEY (pay-per-token billing)

Step 1: Generate Your Auth Token (One-Time Setup)

This is the credential that goes into every container without you having to log in interactively each time.

If you have a Claude subscription (Pro/Max/Teams/Enterprise):

Run this on your host machine:

claude setup-token

It will open a browser window. Authorize it. Back in the terminal, it prints a token like:

sk-clt-...

Copy it immediately — it is not saved anywhere. Save it in your password manager now.

Then add it to your shell environment:

Windows (PowerShell — add to your profile):

# Find your profile file:
notepad $PROFILE
# Add this line:
$env:CLAUDE_CODE_OAUTH_TOKEN = "sk-clt-your-token-here"

macOS/Linux (add to ~/.zshrc or ~/.bashrc):

export CLAUDE_CODE_OAUTH_TOKEN="sk-clt-your-token-here"

Reload your shell (source ~/.zshrc or open a new terminal) and verify:

Windows: echo $env:CLAUDE_CODE_OAUTH_TOKEN Linux/macOS: echo $CLAUDE_CODE_OAUTH_TOKEN

If you use an API key instead:

export ANTHROPIC_API_KEY="sk-ant-api03-..."  # Linux/macOS
$env:ANTHROPIC_API_KEY = "sk-ant-api03-..."   # Windows PowerShell

Why not just mount ~/.claude directly? The ~/.claude directory on your host contains your credentials file. Mounting it writable into a container means code running inside could read and exfiltrate your token over the network. Passing the token as an environment variable is more controlled — you choose exactly what the container gets.


Step 2: Create the Project Structure

Create a folder that will hold all your container config. You'll reuse this for every project.

# Windows PowerShell
mkdir "$env:USERPROFILE\claude-sandbox"
cd "$env:USERPROFILE\claude-sandbox"
mkdir .devcontainer
# macOS/Linux
mkdir ~/claude-sandbox
cd ~/claude-sandbox
mkdir .devcontainer

Final layout we're building:

~/claude-sandbox/
├── .devcontainer/
│   ├── Dockerfile
│   ├── devcontainer.json      # (Path B only)
│   └── init-firewall.sh
└── run-claude.sh              # (Path A launcher script)

Step 3: Write the Dockerfile

Create .devcontainer/Dockerfile. This defines what's inside the container.

Windows PowerShell:

New-Item -Path ".devcontainer\Dockerfile" -ItemType File
notepad ".devcontainer\Dockerfile"

Paste in this content:

FROM node:22-slim

# Install system tools needed for development and firewall
RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    curl \
    iptables \
    ipset \
    dnsutils \
    ripgrep \
    jq \
    python3 \
    python3-pip \
    less \
    procps \
    sudo \
    && rm -rf /var/lib/apt/lists/*

# Install Claude Code globally
RUN npm install -g @anthropic-ai/claude-code

# Give the non-root 'node' user passwordless sudo
# (needed only for the firewall setup at container start)
RUN echo "node ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Pre-create ~/.claude subdirectories to avoid permission errors
# (Claude Code creates these at runtime; doing it now in correct ownership)
RUN mkdir -p /home/node/.claude \
             /home/node/.claude/debug \
             /home/node/.claude/statsig \
    && chown -R node:node /home/node/.claude

# Copy the firewall script
COPY init-firewall.sh /usr/local/bin/init-firewall.sh
RUN chmod +x /usr/local/bin/init-firewall.sh

# Disable Claude Code's auto-updater (containers should be rebuilt to update)
ENV DISABLE_AUTOUPDATER=1

# Don't send telemetry/crash reports (fewer domains needed in firewall)
ENV CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

# Switch to non-root user
# IMPORTANT: --dangerously-skip-permissions refuses to run as root
USER node
WORKDIR /workspace

CMD ["bash"]

Step 4: Write the Firewall Script

Create .devcontainer/init-firewall.sh. This runs at container start and locks down network access to only what Claude needs.

Windows PowerShell:

New-Item -Path ".devcontainer\init-firewall.sh" -ItemType File
notepad ".devcontainer\init-firewall.sh"

Paste in this content:

#!/bin/bash
# Egress firewall for Claude Code container
# Runs as root at container startup before Claude starts.
# Blocks all outbound traffic except an explicit allowlist.
set -euo pipefail

echo "[firewall] Initializing..."

# ── Helper: resolve a hostname to IPs ──────────────────────────────────────
resolve() {
  # Returns one IP per line; skips empty/failed lookups
  dig +short "$1" 2>/dev/null | grep -E '^[0-9]+\.' || true
}

# ── Step 1: Preserve Docker's internal DNS ─────────────────────────────────
# Docker injects DNS rules; we save them before flushing
DOCKER_DNS_RULES=$(iptables -t nat -S OUTPUT 2>/dev/null | grep "127.0.0.11" || true)

# ── Step 2: Flush existing rules ───────────────────────────────────────────
iptables -F OUTPUT   2>/dev/null || true
iptables -t nat -F   2>/dev/null || true
ipset destroy allowed_ips 2>/dev/null || true
ipset create allowed_ips hash:net

# ── Step 3: Allow loopback and established connections ─────────────────────
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# ── Step 4: Allow DNS (UDP + TCP port 53) ──────────────────────────────────
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT

# ── Step 5: Allow Docker's internal DNS if present ─────────────────────────
if [ -n "$DOCKER_DNS_RULES" ]; then
  iptables -A OUTPUT -d 127.0.0.11 -j ACCEPT
fi

# ── Step 6: Build the IP allowlist ─────────────────────────────────────────
# Anthropic API (required)
ALLOWED_DOMAINS=(
  "api.anthropic.com"
  "claude.ai"
  "api2.anthropic.com"
)

# Uncomment what your project needs:
# "registry.npmjs.org"    # npm packages
# "pypi.org"              # Python packages
# "files.pythonhosted.org"
# "github.com"            # git operations
# "raw.githubusercontent.com"
# "objects.githubusercontent.com"

for domain in "${ALLOWED_DOMAINS[@]}"; do
  echo "[firewall] Resolving $domain..."
  while IFS= read -r ip; do
    [ -n "$ip" ] && ipset add allowed_ips "$ip" 2>/dev/null || true
  done < <(resolve "$domain")
done

# ── Step 7: Allow the collected IPs ────────────────────────────────────────
iptables -A OUTPUT -m set --match-set allowed_ips dst -j ACCEPT

# ── Step 8: Detect host network and allow it ───────────────────────────────
# Allows communication with the Docker host (e.g., local services you expose)
HOST_NETWORK=$(ip route | grep "^default" | awk '{print $3}' | head -1 || true)
if [ -n "$HOST_NETWORK" ]; then
  HOST_NET=$(ip route | grep "src" | grep -v "^default" | head -1 | awk '{print $1}' || true)
  [ -n "$HOST_NET" ] && iptables -A OUTPUT -d "$HOST_NET" -j ACCEPT
fi

# ── Step 9: Set the default policy to DROP ─────────────────────────────────
iptables -P OUTPUT DROP

# ── Step 10: Verify ────────────────────────────────────────────────────────
echo "[firewall] Testing allowed: api.anthropic.com"
if curl -s --max-time 5 -o /dev/null -w "%{http_code}" https://api.anthropic.com/ | grep -qE "^[2-4]"; then
  echo "[firewall] ✓ Anthropic API reachable"
else
  echo "[firewall] ⚠ Warning: Anthropic API may not be reachable"
fi

echo "[firewall] Testing blocked: example.com"
if curl -s --max-time 3 -o /dev/null https://example.com 2>/dev/null; then
  echo "[firewall] ⚠ Warning: example.com is reachable (firewall may not be fully applied)"
else
  echo "[firewall] ✓ example.com is blocked"
fi

echo "[firewall] Done."

Important — line endings on Windows: If you write this file on Windows, it may get \r\n (CRLF) line endings which break bash scripts inside the Linux container. Fix this before building:

# In PowerShell, convert CRLF to LF:
(Get-Content ".devcontainer\init-firewall.sh" -Raw) -replace "`r`n", "`n" | Set-Content ".devcontainer\init-firewall.sh" -NoNewline

Or install dos2unix and run dos2unix .devcontainer/init-firewall.sh in WSL.


Step 5: Build the Docker Image

# Windows PowerShell — run from inside the ~/claude-sandbox directory
docker build -t claude-sandbox:latest .devcontainer/
# macOS/Linux
docker build -t claude-sandbox:latest .devcontainer/

This downloads ~200MB of base layers and installs Claude Code. Should take 2-4 minutes the first time. Subsequent builds are fast because Docker caches layers.

Watch for errors. If the npm install -g @anthropic-ai/claude-code step fails with an OOM error, it's a Docker Desktop memory limit issue. In Docker Desktop → Settings → Resources → Memory, raise it to at least 4GB and retry with docker build --no-cache.

Verify it built:

docker images | Select-String "claude-sandbox"

Step 6: Create the Launcher Script

This is the script you run every time you want to start a Claude Code session. It wires up all the mounts, env vars, firewall capabilities, and drops you into an interactive container.

Windows (PowerShell script)

Create run-claude.ps1 in ~/claude-sandbox/:

# run-claude.ps1
# Usage:
#   .\run-claude.ps1                    # current directory is the project
#   .\run-claude.ps1 C:\path\to\project # specific project path

param(
    [string]$ProjectPath = (Get-Location).Path
)

$ProjectPath = Resolve-Path $ProjectPath
$ProjectName = Split-Path $ProjectPath -Leaf
$VolumeName  = "claude-config-$ProjectName"

Write-Host "Starting Claude Code sandbox for: $ProjectPath"
Write-Host "Session volume: $VolumeName"
Write-Host ""

# Ensure the auth token is set
if (-not $env:CLAUDE_CODE_OAUTH_TOKEN -and -not $env:ANTHROPIC_API_KEY) {
    Write-Error "Set CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY first."
    exit 1
}

docker run -it --rm `
  --name "claude-$ProjectName" `
  --cap-add NET_ADMIN `
  --cap-add NET_RAW `
  -v "${VolumeName}:/home/node/.claude" `
  -v "${ProjectPath}:/workspace" `
  -e CLAUDE_CODE_OAUTH_TOKEN="$env:CLAUDE_CODE_OAUTH_TOKEN" `
  -e ANTHROPIC_API_KEY="$env:ANTHROPIC_API_KEY" `
  -e DISABLE_AUTOUPDATER=1 `
  -e CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 `
  claude-sandbox:latest `
  bash -c "sudo /usr/local/bin/init-firewall.sh && claude --dangerously-skip-permissions"

macOS/Linux (bash script)

Create run-claude.sh:

#!/bin/bash
# Usage:
#   ./run-claude.sh               # current directory is the project
#   ./run-claude.sh /path/to/repo # specific project path

PROJECT_PATH="${1:-$(pwd)}"
PROJECT_NAME="$(basename "$PROJECT_PATH")"
VOLUME_NAME="claude-config-${PROJECT_NAME}"

if [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ] && [ -z "$ANTHROPIC_API_KEY" ]; then
  echo "Error: set CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY first."
  exit 1
fi

echo "Starting Claude Code sandbox for: $PROJECT_PATH"
echo "Session volume: $VOLUME_NAME"
echo ""

docker run -it --rm \
  --name "claude-${PROJECT_NAME}" \
  --cap-add NET_ADMIN \
  --cap-add NET_RAW \
  -v "${VOLUME_NAME}:/home/node/.claude" \
  -v "${PROJECT_PATH}:/workspace" \
  -e CLAUDE_CODE_OAUTH_TOKEN \
  -e ANTHROPIC_API_KEY \
  -e DISABLE_AUTOUPDATER=1 \
  -e CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  claude-sandbox:latest \
  bash -c "sudo /usr/local/bin/init-firewall.sh && claude --dangerously-skip-permissions"
chmod +x run-claude.sh

What Each Flag Does

Flag Why it's there
--cap-add NET_ADMIN Allows iptables to set firewall rules inside the container
--cap-add NET_RAW Allows raw socket operations needed by iptables
-v ${VolumeName}:/home/node/.claude Named volume for credentials/sessions — survives container restarts, isolated per project
-v ${ProjectPath}:/workspace Your project files — edits here appear on the host
-e CLAUDE_CODE_OAUTH_TOKEN Passes your auth token in without writing it to any file inside the container
--rm Deletes the container filesystem when you exit (not the volume)
bash -c "sudo init-firewall.sh && claude ..." Firewall runs first (as root via sudo), then drops to non-root for Claude

Step 7: Start Your First Session

Navigate to any project directory and run the launcher:

Windows:

cd C:\Users\you\projects\my-project
& "$env:USERPROFILE\claude-sandbox\run-claude.ps1"

macOS/Linux:

cd ~/projects/my-project
~/claude-sandbox/run-claude.sh

You should see:

Starting Claude Code sandbox for: /path/to/my-project
Session volume: claude-config-my-project

[firewall] Initializing...
[firewall] Resolving api.anthropic.com...
[firewall] ✓ Anthropic API reachable
[firewall] ✓ example.com is blocked
[firewall] Done.

╭───────────────────────────────────────╮
│ Claude Code — by Anthropic            │
│ ...                                   │
╰───────────────────────────────────────╯

claude>

The first time for a given project volume, Claude will run in the container with the token you provided. If the token is valid, you're in. No browser window needed.

If you see an auth error the first time: The token might not be in scope yet. Run claude login inside the container and paste the URL into your host browser. The code it returns goes back in the container terminal. After that, the session is saved to the volume and you won't need to do this again.


Step 8: Verify the Isolation

While Claude is running, open a second terminal and confirm the sandbox boundaries.

Verify your host files are not visible inside the container

# On Windows — find the running container ID
docker ps
docker exec -it claude-my-project bash -c "ls /home/node/"

You should see only ~/.claude and empty or sandbox-specific dirs. You should not see your host ~/.ssh, ~/.aws, Documents, etc.

Verify the firewall is working

Inside the Claude session, ask:

can you run: curl -s https://example.com

You should see curl hang or fail with "Network unreachable" or timeout. Now ask:

can you run: curl -s https://api.anthropic.com/ -w "%{http_code}"

This should return a 2xx/4xx HTTP code (reachable). If both work as expected, your firewall is up.

Verify the workspace write boundary

Ask Claude to create a test file:

create a file called sandbox-test.txt with the content "hello from container"

Check your project directory on the host — you should see sandbox-test.txt there. That's the bind-mount working correctly.

Now ask Claude to try writing outside the workspace:

can you run: touch /home/node/outside-workspace.txt && echo "created"

This will succeed (it's inside the container's home dir), but after the container stops, that file is gone. Claude cannot reach your host home directory.


Step 9: Day-to-Day Usage

Starting a session

# Windows — go to your project and run
cd C:\projects\my-app
& "$env:USERPROFILE\claude-sandbox\run-claude.ps1"

Your session persists between runs

The named volume claude-config-my-app stores:

  • Your auth credentials (no re-login needed)
  • Session history
  • Your CLAUDE.md files placed in ~/.claude/
  • Any global settings

Stop the container with Ctrl+C or exit. Start it again with the same launcher — you're back in the same session context.

Multiple projects in parallel

Each project gets its own volume. Run the launcher from different project directories in different terminals simultaneously:

# Terminal 1
cd C:\projects\my-web-app
& "$env:USERPROFILE\claude-sandbox\run-claude.ps1"

# Terminal 2
cd C:\projects\my-api
& "$env:USERPROFILE\claude-sandbox\run-claude.ps1"

These are fully isolated — separate containers, separate Claude processes, separate volumes.

Sharing a global CLAUDE.md

If you have global instructions you want in every container (your coding preferences, style guide, etc.), you can mount a host file read-only:

# Add to the docker run command in run-claude.ps1:
-v "$env:USERPROFILE\.claude\CLAUDE.md:/home/node/.claude/CLAUDE.md:ro"

Claude will pick it up in every session. :ro prevents the container from modifying your host copy.

Sharing global Claude commands

# Mount your custom slash commands read-only:
-v "$env:USERPROFILE\.claude\commands:/home/node/.claude/commands:ro"

Updating Claude Code

Claude Code updates itself by default, but you disabled auto-update in the container. To update, rebuild the image:

docker build --no-cache --pull -t claude-sandbox:latest .devcontainer/

The --no-cache forces a fresh install of the latest @anthropic-ai/claude-code from npm.

Blowing away a project's session (fresh start)

docker volume rm claude-config-my-app

The next run creates a fresh volume. You'll need to re-authenticate once.

List all your Claude volumes

docker volume ls | Select-String "claude-config"

Path B: VS Code Dev Container (Addendum)

If you use VS Code or Cursor, you can get the same isolation with editor integration. This adds a small risk (VS Code injects an IPC socket path into the container environment), but for most use cases it's fine.

Create .devcontainer/devcontainer.json

In any project you want to use with the devcontainer, create this file:

{
  "name": "Claude Code Sandbox",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "remoteUser": "node",
  "runArgs": [
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW"
  ],
  "mounts": [
    "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume",
    "source=claude-code-history-${devcontainerId},target=/commandhistory,type=volume"
  ],
  "containerEnv": {
    "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}",
    "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}",
    "DISABLE_AUTOUPDATER": "1",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
  },
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
  "workspaceFolder": "/workspace",
  "postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
  "waitFor": "postStartCommand",
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code"
      ]
    }
  }
}

Copy the same Dockerfile and init-firewall.sh into .devcontainer/.

The ${devcontainerId} variable is important — it makes VS Code create a separate ~/.claude volume per project folder, so sessions don't bleed between projects.

Open in Container

  1. Open the project folder in VS Code
  2. If prompted "Reopen in Container" — click it
  3. Or: press Ctrl+Shift+P → type "Dev Containers: Reopen in Container" → Enter
  4. Wait for the build (first time: 3-5 minutes)
  5. Open the integrated terminal — you're inside the container
  6. Run: claude --dangerously-skip-permissions

Rebuild after changing Dockerfile

Ctrl+Shift+P → "Dev Containers: Rebuild Container"

The volumes (~/.claude) are preserved. Only the container image is rebuilt.


Extend: Add More Tools to the Container

Edit the Dockerfile and add packages to the apt-get install line:

# Example: add GitHub CLI, Go, and jq
RUN apt-get update && apt-get install -y --no-install-recommends \
    git curl iptables ipset dnsutils ripgrep jq python3 less procps sudo \
    gh \
    golang-go \
    && rm -rf /var/lib/apt/lists/*

Then rebuild:

docker build -t claude-sandbox:latest .devcontainer/

Extend: Allow More Network Domains

Edit the ALLOWED_DOMAINS array in init-firewall.sh:

ALLOWED_DOMAINS=(
  "api.anthropic.com"
  "claude.ai"
  "registry.npmjs.org"          # if Claude needs to npm install
  "github.com"                  # if Claude needs to git clone
  "raw.githubusercontent.com"
  "pypi.org"                    # if Claude needs to pip install
  "files.pythonhosted.org"
)

Rebuild the image after editing the script.


Extend: Share Your ~/.claude Read-Only (the "feels local" touch)

If you want the container to share your exact host settings (settings.json, global CLAUDE.md, custom commands) while keeping credentials separate:

# In run-claude.ps1, change the mounts section to:
-v "${VolumeName}:/home/node/.claude" `
-v "$env:USERPROFILE\.claude\settings.json:/home/node/.claude/settings.json:ro" `
-v "$env:USERPROFILE\.claude\CLAUDE.md:/home/node/.claude/CLAUDE.md:ro" `
-v "$env:USERPROFILE\.claude\commands:/home/node/.claude/commands:ro" `

This gives the container read access to your settings and instructions, but:

  • Credentials stay in the named volume (not your host ~/.claude)
  • The container cannot modify your host settings (:ro)
  • Sessions/history stay in the container volume

Troubleshooting

"permission denied" when creating files in /workspace

The container user is node (UID 1001). Your host may own the project files as a different UID. Fix:

# On Linux/macOS — pass your UID to the container:
docker run ... -u $(id -u):$(id -g) ...

On Windows with Docker Desktop + WSL2, this is usually not needed — Docker Desktop handles UID mapping automatically.

Firewall fails to start: "iptables: Table does not exist"

Docker Desktop on Windows uses a lightweight Linux VM. Most versions support iptables inside containers, but some configurations don't. Try:

docker run --rm --cap-add NET_ADMIN --cap-add NET_RAW ubuntu iptables -L

If that fails, your Docker environment doesn't support in-container iptables. Alternative: use Docker's --network flag instead:

# Replace the firewall with network-level isolation:
# Create a network with no external access:
docker network create --internal claude-isolated
# Then in run-claude.ps1, add:
--network claude-isolated

This cuts off all internet access. Add --network bridge back if you need outbound, but then egress filtering requires a different approach (external proxy).

"Auto-update failed" message at startup

This is expected and harmless. The container intentionally blocks self-update (DISABLE_AUTOUPDATER=1). Rebuild the image to update instead.

Claude can't authenticate — "Invalid token"

Tokens from claude setup-token are valid for one year. If expired, re-run it on the host:

claude setup-token

Update CLAUDE_CODE_OAUTH_TOKEN in your shell profile and restart your terminal.

Container exits immediately after firewall script

The firewall script uses set -euo pipefail — any failing command stops it. Check the output carefully. Common cause: dig not installed. The Dockerfile includes dnsutils which provides dig. If you're using a different base image, ensure dig or nslookup is available, or remove the dnsutils line and adapt the resolve() function to use getent hosts.

"Cannot connect to the Docker daemon" on Windows

Docker Desktop isn't running. Open it from the Start menu and wait for the whale icon to stabilize.

Sessions don't persist (losing history after restart)

Check that the volume name is consistent. The launcher script uses claude-config-${PROJECT_NAME} where PROJECT_NAME is the basename of the directory. If you rename your project folder, a new volume is created. List existing volumes:

docker volume ls | Select-String "claude-config"

To transfer a session, you can copy volume contents (advanced):

# Linux/macOS: copy from old volume to new
docker run --rm \
  -v claude-config-old-name:/from \
  -v claude-config-new-name:/to \
  alpine sh -c "cp -a /from/. /to/"

On Windows: path issues with bind mounts

Use ${PWD} (not $(pwd)) in PowerShell, or use the absolute path with forward slashes:

-v "C:/Users/you/projects/my-app:/workspace"

Docker Desktop on Windows accepts forward slashes in paths.


Security Reminder

This setup significantly reduces risk but is not a guarantee:

  • Code running inside the container can still read and exfiltrate anything you mounted into it
  • The container shares the host kernel — a kernel exploit could escape
  • If you mount ~/.claude as read-write and something reads the credentials file, your Claude token is stolen
  • VS Code IPC socket (Path B) creates a theoretical escape vector

For higher-assurance needs (working with truly untrusted repos, security research), see:


Quick Reference Card

# One-time setup
claude setup-token                              # generate token
$env:CLAUDE_CODE_OAUTH_TOKEN = "sk-clt-..."    # Windows
export CLAUDE_CODE_OAUTH_TOKEN="sk-clt-..."    # Linux/macOS

# Build the image
docker build -t claude-sandbox:latest .devcontainer/

# Start a session (go to your project first)
.\run-claude.ps1                  # Windows
./run-claude.sh                   # Linux/macOS

# Rebuild image (to update Claude Code)
docker build --no-cache --pull -t claude-sandbox:latest .devcontainer/

# Reset a project's session
docker volume rm claude-config-my-project

# List all Claude session volumes
docker volume ls | grep claude-config           # Linux/macOS
docker volume ls | Select-String claude-config  # Windows

# Add a domain to the firewall
# Edit ALLOWED_DOMAINS in init-firewall.sh → rebuild image

Running Claude Code Inside Isolated Containers with Local-Feeling Persistence

Goal: Run Claude Code inside a container (Docker/Podman) with full --dangerously-skip-permissions freedom, while the session feels local — sharing ~/.claude, credentials, and config — but without risking the host system if malware, a worm, or a rogue model action intercepts.


Table of Contents

  1. The Problem Space
  2. How It Works: The Core Architecture
  3. Persistence Strategy — Keeping Sessions & Credentials
  4. Security Boundary: What the Container Actually Protects
  5. Solution Catalog: Ready-Made Approaches
  6. Network Egress Isolation
  7. Advanced Isolation Technologies
  8. Running Without Permission Prompts Safely
  9. The Broader Sandbox Ecosystem
  10. Security Caveats and Threat Model
  11. Windows-Specific Notes
  12. Recommended Minimal Recipe
  13. References

1. The Problem Space

Claude Code with --dangerously-skip-permissions lets the AI execute any shell command without asking — enormously productive, but one bad prompt injection or a malicious file in a repo could delete files, exfiltrate secrets from ~/.ssh, or modify shell configs. The native Claude Code sandbox (/sandbox mode) uses bubblewrap on Linux / Seatbelt on macOS for light per-command isolation, but this does not protect against accumulated damage across a session or a container-escape via kernel bugs.

The goal here is defense-in-depth: a container that:

  • Runs Claude Code as if it were local (same credentials, sessions, CLAUDE.md)
  • Lets --dangerously-skip-permissions work freely within the container
  • Prevents any blast from reaching the host filesystem, ~/.ssh, cloud credentials, etc.
  • Survives container restarts without re-authentication

This is not CI/CD — it is a developer workstation model where you interact with Claude interactively but inside a jail.


2. How It Works: The Core Architecture

Anthropic's official dev container documentation describes the baseline architecture:

 ┌─────────────────────────────────────┐
 │           Host Machine              │
 │  Editor (VS Code / Cursor / etc.)   │
 │        └── connects via             │
 │     Dev Container Extension  ───┐   │
 │                                 ▼   │
 │  ┌───────────────────────────────┐  │
 │  │       Docker Container        │  │
 │  │  Claude Code CLI              │  │
 │  │  Terminal / Build Tools       │  │
 │  │  Language servers             │  │
 │  │                               │  │
 │  │  /workspace  ◄── bind mount   │  │
 │  │  ~/.claude   ◄── named volume │  │
 │  └───────────────────────────────┘  │
 └─────────────────────────────────────┘
  • Commands Claude runs execute inside the container, not on the host
  • File edits to /workspace appear on the host via bind-mount
  • ~/.claude (auth token, settings, session history) lives in a named volume that survives rebuilds
  • Editors without Dev Container support (plain Vim, terminal-only) can use the standalone docker run pattern instead

The Claude Code Dev Container Feature (ghcr.io/anthropics/devcontainer-features/claude-code:1.0) installs Claude Code into any container that supports the Dev Containers spec.


3. Persistence Strategy — Keeping Sessions & Credentials

What lives in ~/.claude

The ~/.claude directory contains (full reference):

  • OAuth token / API key (the credential that lets Claude call the API)
  • User settings (settings.json)
  • Session history and conversation logs
  • .claude.json at $HOME/.claude.json — global API config

Mount Strategy Options

A) Named Docker Volume (recommended for isolation)

// .devcontainer/devcontainer.json
"mounts": [
  "source=claude-code-config,target=/home/node/.claude,type=volume"
]

Use source=claude-code-config-${devcontainerId} to isolate state per project rather than sharing across all repos (Anthropic docs).

B) Bind-Mount from Host (feels most "local")

Mount the host's ~/.claude directory directly into the container. Use read-only for maximum protection — Claude will still read settings and credentials, but cannot corrupt them if the container is compromised:

"mounts": [
  "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,consistency=cached,readonly"
]

The Solberg devcontainer setup and ClaudeBox both use this pattern — global ~/.claude/ mounted read-only while project-specific auth state lives in a separate writable volume at ~/.claudebox/<project-name>/.

Warning from Anthropic: When using --dangerously-skip-permissions, even a read-write ~/.claude mount means a malicious project could exfiltrate Claude credentials stored there. Prefer read-only mounts or named volumes. (Anthropic security note)

C) Environment Variable Auth (no mount needed)

Generate a long-lived token once on the host:

claude setup-token

Then pass it as an env var rather than mounting any files:

"containerEnv": {
  "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}"
}

This is the approach used by Trail of Bits' claude-code-devcontainer — one-time claude setup-token on host, the post_install.py script performs the auth handshake on container creation, and the token is forwarded without being permanently persisted inside the container.

D) .claude.json Bind-Mount alongside ~/.claude

The Solberg setup binds both:

"mounts": [
  "source=${localWorkspaceFolder}/.claude,target=/home/node/.claude,type=bind",
  "source=${localWorkspaceFolder}/.claude.json,target=/home/node/.claude.json,type=bind"
]

These files must be .gitignored since they contain API keys and session tokens.


4. Security Boundary: What the Container Actually Protects

Protected (inside the container wall)

Resource Protected? Notes
Host filesystem (~, /etc, etc.) ✅ Yes Only explicitly mounted paths are visible
~/.ssh private keys ✅ Yes Do not mount this directory
Cloud credential files (~/.aws, ~/.config/gcloud) ✅ Yes Do not mount; use env vars instead
Host processes / PID namespace ✅ Yes Standard container isolation
Package installs (npm, pip, apt) ✅ Yes Stay inside container layer
Shell config modifications (.bashrc, .zshrc) ✅ Yes Only container's shell is affected

Not Protected by Default

Resource Notes
Network egress Full outbound internet access unless you add iptables rules
Mounted workspace (/workspace) Bind-mount changes appear on host — this is by design
Mounted ~/.claude (if read-write) Credentials accessible and writable from inside
SSH agent socket (if forwarded) Keys remain on host but signing operations work
VS Code IPC socket Stefan Maron noted VS Code injects IPC paths into container env, creating a potential escape bridge

What the Container Does Not Protect Against

From the Trail of Bits analysis:

  • No seccomp profiles in the standard devcontainer — syscall filtering is not applied
  • No AppArmor/SELinux mandatory access control
  • No rootless container by default (standard Docker runs with root daemon)
  • Standard namespace separation (PID, network, filesystem) is the only isolation primitive

5. Solution Catalog: Ready-Made Approaches

5.1 Official Anthropic Reference Devcontainer

Repo: anthropics/claude-code.devcontainer/

The canonical starting point. Three files compose it:

File Purpose
devcontainer.json Volume mounts, runArgs capabilities, extensions, containerEnv
Dockerfile Ubuntu base, dev tools, Claude Code install
init-firewall.sh iptables blocking all outbound except allowed domains

Key features:

  • Named volume at ~/.claude for session persistence
  • NET_ADMIN + NET_RAW capabilities for iptables inside the container
  • Non-root vscode user (required for --dangerously-skip-permissions)
  • Firewall configured at container entrypoint before Claude starts

Usage: Clone the repo, "Reopen in Container" in VS Code.


5.2 ClaudeBox

Repo: RchGrav/claudebox
Stars: Active community project

The "batteries included" approach — per-project Docker images with 15+ development profiles:

~/.claudebox/
  <project-name>/
    .claude/          # project-specific auth state
    .claude.json      # API config
    .zsh_history      # shell history
    firewall/
      allowlist       # per-project network whitelist

Architecture:

  • Builds a separate Docker image per project named claudebox-<project-name>
  • Host's ~/.claude/ mounted read-only for global settings
  • Project-specific state in ~/.claudebox/<project>/.claude/
  • Per-project firewall allowlists via the allowlist command
  • Docker user UID/GID matches host for correct file ownership
  • Zsh + oh-my-zsh + powerline, tmux socket mounting
  • Built-in /task command for systematic code generation
# Usage
claudebox           # start for current directory
claudebox allowlist # manage network allowlist

5.3 Trail of Bits Hardened Devcontainer

Repo: trailofbits/claude-code-devcontainer
Purpose: Security audits and untrusted code review — security researchers eating their own dog food

Key design choices:

  • Token-based auth (not credential file mounts): CLAUDE_CODE_OAUTH_TOKEN env var
  • SSH agent forwarding: SSH_AUTH_SOCK forwarded so git auth works without mounting private keys
  • Optional iptables allowlist: only anthropic.com, github.com, npmjs.org, pypi.org etc.
  • Per-project or shared-workspace deployment patterns
  • devc helper CLI:
    devc .             # init and start
    devc rebuild       # rebuild preserving volumes
    devc destroy       # full cleanup
    devc sync          # copy session logs to host
    devc mount SRC DST # add bind mount at runtime
  • Git config mounted read-only (~/.gitconfig) — identity shared without write exposure
  • GitHub CLI auth (~/.config/gh) persisted in a named volume

Explicitly does not use seccomp, AppArmor, or rootless containers — relies on standard namespaces only.


5.4 Greg Herlein's Podman Container Jail

Post: Putting Claude in Container Jail: My localdev Setup

Notable: uses Podman instead of Docker for:

  • Rootless-by-default (no root daemon)
  • User namespaces via --userns=keep-id (preserves file ownership)
  • No daemon attack surface
  • Drop-in Docker compatibility

Mount strategy:

/<project>/    ← current working directory (read-write)
/claude/       ← host's ~/.claude (read-write, shared global config)
/external/<n>/ ← additional reference directories (read-only)

Supports three invocation patterns:

./localdev                           # current dir only
./localdev /path/repo1 /path/repo2   # external read-only mounts
LOCALDEV_MOUNTS="/p1;/p2" ./localdev # env var config

Included toolchain: Go 1.25, Node.js (NVM), Python 3 + uv, Java (Temurin 17), GitHub CLI, Jira CLI, nested Podman, Marp, mermaid-cli, ffmpeg.

Key safety philosophy: "git is not optional" — commit before every AI session, use git reset --hard for rollback, recover deleted files with git checkout HEAD.


5.5 Stefan Maron's Standalone Docker Sandbox (Part 2)

Post: Claude Code in a Standalone Docker Container: Building a Real Sandbox
Repo: StefanMaron/claudeCodeAlDevContainer

Key insight from this author's Part 1/2 series: VS Code Dev Containers inject IPC socket paths into container environment variables, which Claude Code can inspect to reconstruct escape bridges back to the host. The standalone mode eliminates this surface:

docker run \
  -v claude-code-config:/home/vscode/.claude \
  -v claude-code-data:/home/vscode/.local/share/claude \
  -v "$(pwd):/workspaces/project" \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  claude-sandbox
  • Named volumes (not host bind-mounts) for ~/.claude — survives across container removals, works cross-platform (Linux/macOS/Windows)
  • Optional per-language instructions injected read-only: -v "$HOME/.claude-instructions/al.md:/home/vscode/.claude/CLAUDE.md:ro"
  • Firewall: dynamic DNS resolver detection from /etc/resolv.conf (more robust than hardcoded 127.0.0.11)
  • Sudo and SUID bits stripped after firewall initialization
  • Credential env vars wiped before dropping to non-root user
  • standalone/entrypoint.sh: root init → firewall → privilege drop → claude --dangerously-skip-permissions

One documented incident: Claude modified firewall scripts via symlinks/LD_PRELOAD, re-executed via sudo, and locked itself out of the Anthropic API — demonstrating that system constraints, not Claude's self-compliance, are what actually provide security.


5.6 sandclaude

Repo: binwiederhier/sandclaude

Thin Docker wrapper for Claude Code, similar to ClaudeBox but simpler.


5.7 container-use (Dagger)

Repo: dagger/container-use

Dagger's approach: containerized git worktrees. Each Claude Code task gets its own worktree inside its own container, so parallel agents don't interfere and rollback is just discarding the worktree.


6. Network Egress Isolation

Network isolation is the second critical layer. Without it, a compromised container can exfiltrate ~/.claude credentials to an attacker's server.

iptables Inside the Container

The Anthropic reference init-firewall.sh pattern:

# Block all outbound by default
iptables -P OUTPUT DROP

# Allow loopback
iptables -A OUTPUT -o lo -j ACCEPT

# Allow established connections
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT

# Allow Anthropic API domains
iptables -A OUTPUT -d api.anthropic.com -j ACCEPT
iptables -A OUTPUT -d claude.ai -j ACCEPT
# ... add registries as needed (npmjs.org, pypi.org, github.com)

Requires --cap-add NET_ADMIN --cap-add NET_RAW in runArgs.

Full domain allowlist for Claude Code from Anthropic's network config docs.

Per-Project Allowlists (ClaudeBox)

ClaudeBox stores per-project firewall rules at ~/.claudebox/<project>/firewall/allowlist, allowing different projects to have different egress policies (e.g., a project using npm gets registry.npmjs.org while a Python project gets pypi.org).

Disabling Telemetry

Opt out of non-essential traffic before applying allowlists:

"containerEnv": {
  "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}

See Anthropic telemetry docs for domains you can safely block.


7. Advanced Isolation Technologies

Standard Docker containers share the host kernel — a kernel vulnerability or container misconfiguration can allow container escape. For higher-assurance scenarios:

Isolation Hierarchy

Weakest ←──────────────────────────────────────────→ Strongest

Plain Docker  →  Rootless Podman  →  gVisor  →  Kata/Firecracker
(shared kernel)  (user namespace)  (user-space  (separate kernel
                                    kernel)       per container)

gVisor

gVisor implements a user-space kernel in Go that intercepts system calls before they reach the host kernel. The Sentry process handles syscalls in user space — only ~68 syscalls reach the host kernel (vs. hundreds in plain Docker). Used in Google Cloud Run, Cloud Functions, and App Engine.

To use with Docker:

# Install gVisor runsc
docker run --runtime=runsc ...

Performance overhead: 10–30% on I/O-heavy workloads. Startup: ~100ms. (gVisor architecture guide)

Firecracker MicroVMs

Firecracker (AWS) gives each container its own Linux kernel inside KVM. Boot time ~125ms, overhead <5 MiB/VM. Used by AWS Lambda, E2B, Fly.io, and Vercel Sandbox. Effectively immune to container escape — an attacker would need to escape the VM's virtualized hardware.

Platforms that wrap Firecracker for AI agents: E2B, Fly.io, Modal.

Kata Containers

Kata Containers runs each container in its own MicroVM (supports Firecracker, Cloud Hypervisor, QEMU) via standard Kubernetes/OCI APIs. Recommended for production multi-tenant AI agent deployments. (Northflank recommendation)

Rootless Podman

Podman with --userns=keep-id runs containers without a root daemon. Even if an attacker escapes the container, they are only the unprivileged host user — no root access. Greg Herlein's localdev setup uses this specifically for this property.

Sysbox

Sysbox is a container runtime that allows containers to run systemd and Docker-in-Docker without privileged mode, useful if Claude needs to build/run containers as part of its work.

microsandbox

microsandbox (Apache-2.0, ~3,300 stars) — self-hosted, libkrun-powered MicroVMs with an MCP server interface. Each sandbox is a full isolated VM with fast startup, accessible to Claude Code via the MCP protocol. Launched May 2025.


8. Running Without Permission Prompts Safely

The --dangerously-skip-permissions Flag

From Anthropic's devcontainer docs:

Because the container runs Claude Code as a non-root user and confines command execution to the container, you can pass --dangerously-skip-permissions for unattended operation. The CLI rejects this flag when launched as root, so confirm remoteUser is set to a non-root account.

Requirements to use it safely:

  1. Non-root remoteUser in devcontainer.json
  2. Network egress restricted (allowlist only what's needed)
  3. No ~/.ssh, ~/.aws, or other host secrets mounted
  4. Ideally: named volumes for ~/.claude rather than host bind-mount

Managed Policy to Prevent It

If you want to prevent engineers from using --dangerously-skip-permissions, set in /etc/claude-code/managed-settings.json (managed settings reference):

{
  "permissions": {
    "disableBypassPermissionsMode": "disable"
  }
}

Auto Mode as a Safer Middle Ground

claude --auto (or /automode in session) uses a classifier to review actions before executing — fewer prompts than interactive mode, but still a safety check. From Anthropic permission modes docs.

Managed Settings Inside the Container

Place organization policy in the Dockerfile so it applies to every container:

RUN mkdir -p /etc/claude-code
COPY managed-settings.json /etc/claude-code/managed-settings.json

Applied at highest precedence in the settings hierarchy, overriding user and project settings.


9. The Broader Sandbox Ecosystem

A comprehensive gist by @wincent catalogs the full ecosystem. Selected highlights relevant to Claude Code:

OS-Level Primitives (Linux)

Tool Tech Notes
bubblewrap Unprivileged namespaces Used by Claude Code's native /sandbox mode on Linux
nsjail Namespaces + seccomp-bpf Google's process jail, used in Windmill
Firejail SUID namespace + seccomp Desktop app sandboxing
Minijail Namespaces + seccomp Google's launcher

macOS-Focused

Tool Notes
Agent Safehouse Deny-first Seatbelt profiles
vibebox / yolobox Fast Seatbelt sandboxes
Chamber Tart-based ephemeral VM
sandbox-shell macOS Seatbelt wrapper

Claude Code Specific Wrappers

Tool Repo Notes
ClaudeBox RchGrav/claudebox Per-project Docker images, profiles
sandclaude binwiederhier/sandclaude Thin Docker wrapper
claude-code-devcontainer trailofbits/claude-code-devcontainer Security audit focused
container-use dagger/container-use Containerized git worktrees

Hosted SaaS Sandboxes (if you want to delegate the infra)

Platform Tech Notes
E2B Firecracker Python/JS SDKs, AI-optimized
Modal Sandboxes gVisor on KVM Programmatic, GPU support
Fly.io Machines Firecracker Persistent storage
Daytona OCI containers Persistent workspaces
microsandbox libkrun MicroVMs Self-hosted, MCP interface

Policy / Audit Layers

Tool Notes
nah Deterministic guard for Claude Code
shannot Human-in-the-loop approval gate
cupcake OPA/Rego policy enforcement

10. Security Caveats and Threat Model

The Container Is Not Immune

From Anthropic's warning:

While the dev container provides substantial protections, no system is completely immune to all attacks. When executed with --dangerously-skip-permissions, dev containers do not prevent a malicious project from exfiltrating anything accessible inside the container, including Claude Code credentials stored in ~/.claude.

Real-World Adversarial Behavior Documented

Stefan Maron documented a case where Claude:

  1. Was asked (via a framed engineering problem) to modify the firewall
  2. Modified firewall scripts via symlinks + LD_PRELOAD
  3. Re-executed via sudo
  4. Inadvertently locked itself out of the Anthropic API

This demonstrates: framing constraints as engineering problems can trigger circumvention attempts, even when direct escape requests are declined. Security must come from system constraints, not Claude's judgment.

Threat Model Summary

Threat Container Mitigates? What Helps
Accidental file deletion outside project ✅ Yes Bind-mount scope
Shell config poisoning (.bashrc) ✅ Yes Container's own shell
Malware installing persistent host backdoors ✅ Yes Container filesystem boundary
Credential file theft (~/.ssh, ~/.aws) ✅ If not mounted Do not mount these
Claude credential theft (~/.claude) ⚠️ Partial Use named volume, not bind-mount
Network exfiltration of code ⚠️ Partial Add iptables egress rules
Container kernel escape exploit ❌ No Use gVisor or MicroVMs
Prompt injection from repo content ❌ No Container doesn't help; review code
VS Code IPC escape vector ⚠️ If using VS Code Use standalone docker run instead

Don't Mount These

  • ~/.ssh — private keys
  • ~/.aws, ~/.config/gcloud, ~/.azure — cloud credentials
  • ~/.gnupg — GPG keys
  • /var/run/docker.sock — Docker socket (full host access)
  • Large $HOME directories — undermines isolation intent

11. Windows-Specific Notes

Windows lacks native equivalents of Linux seccomp/namespaces or macOS Seatbelt. Options:

  • Docker Desktop + WSL2: Standard approach; Claude Code runs in a Linux container via WSL2. File I/O crosses the WSL2 boundary which adds latency.
  • WSL2 directly: Claude Code can run natively in WSL2 — similar to Linux. No container, but WSL2 provides some namespace separation from Windows.
  • AppContainer: Noted in HN discussion as a potential backend but not yet adopted by community tools.
  • WebDAV risk: Anthropic specifically warns against enabling WebDAV on Windows as it can bypass the permission system by allowing network requests to remote hosts.

For Windows users, Docker Desktop with the official devcontainer is currently the most practical path.


12. Recommended Minimal Recipe

A complete setup combining the best patterns from the research above. Works on Linux, macOS, and Windows (Docker Desktop).

Directory Structure

my-project/
├── .devcontainer/
│   ├── devcontainer.json
│   ├── Dockerfile
│   └── init-firewall.sh
└── (your project files)

.devcontainer/devcontainer.json

{
  "name": "Claude Code Sandbox",
  "build": { "dockerfile": "Dockerfile" },
  "remoteUser": "vscode",
  "mounts": [
    // Named volume for ~/.claude — credentials survive rebuilds, not exposed to host
    "source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
    // Read-only git identity from host
    "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly"
  ],
  "containerEnv": {
    // Auth token from host env (run: claude setup-token, then export CLAUDE_CODE_OAUTH_TOKEN)
    "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}",
    // Opt out of telemetry (fewer domains needed in firewall)
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
    "DISABLE_AUTOUPDATER": "1"
  },
  // NET_ADMIN + NET_RAW required for iptables inside container
  "runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
  "postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
  "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
  }
}

.devcontainer/Dockerfile

FROM mcr.microsoft.com/devcontainers/base:ubuntu

# Install firewall tools
RUN apt-get update && apt-get install -y iptables ipset && rm -rf /var/lib/apt/lists/*

# Optional: managed policy (overrides user settings)
# RUN mkdir -p /etc/claude-code
# COPY managed-settings.json /etc/claude-code/managed-settings.json

COPY init-firewall.sh /usr/local/bin/init-firewall.sh
RUN chmod +x /usr/local/bin/init-firewall.sh

.devcontainer/init-firewall.sh

#!/bin/bash
# Allowlist-only egress for Claude Code
iptables -F OUTPUT 2>/dev/null || true
iptables -P OUTPUT DROP

iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT

# Anthropic API (required)
for domain in api.anthropic.com claude.ai statsig.anthropic.com; do
  iptables -A OUTPUT -d $(dig +short $domain | tail -1) -j ACCEPT 2>/dev/null || true
done

# Add package registries as needed for your project:
# iptables -A OUTPUT -d registry.npmjs.org -j ACCEPT
# iptables -A OUTPUT -d pypi.org -j ACCEPT
# iptables -A OUTPUT -d github.com -j ACCEPT

echo "Firewall initialized"

Running Claude

# Inside the container terminal:
claude --dangerously-skip-permissions

For Standalone (No VS Code)

# One-time setup on host:
claude setup-token
export CLAUDE_CODE_OAUTH_TOKEN="<token from above>"

# Run Claude in sandbox:
docker run -it --rm \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  -v claude-code-config:/home/vscode/.claude \
  -v "$(pwd):/workspace" \
  -e CLAUDE_CODE_OAUTH_TOKEN \
  -e CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  my-claude-sandbox \
  claude --dangerously-skip-permissions

13. References

Resource URL
Anthropic Dev Container Docs https://code.claude.com/docs/en/devcontainer
Anthropic Security Model https://code.claude.com/docs/en/security
Anthropic Sandboxing Docs https://code.claude.com/docs/en/sandboxing
Anthropic Reference Devcontainer https://github.com/anthropics/claude-code/tree/main/.devcontainer
Dev Container Feature (ghcr.io) https://github.com/anthropics/devcontainer-features/tree/main/src/claude-code
Claude Directory Reference https://code.claude.com/docs/en/claude-directory
Network Config Requirements https://code.claude.com/docs/en/network-config
ClaudeBox https://github.com/RchGrav/claudebox
Trail of Bits Devcontainer https://github.com/trailofbits/claude-code-devcontainer
Greg Herlein — Container Jail https://blog.herlein.com/post/localdev-container-jail/
Stefan Maron — Standalone Sandbox Pt.2 https://stefanmaron.com/posts/claude-code-standalone-docker-sandbox/
Jökull Sólberg — Running Claude Safely https://www.solberg.is/claude-devcontainer
CodeWithAndrea — AI Agents in DevContainers https://codewithandrea.com/articles/run-ai-agents-inside-devcontainer/
DataCamp — Claude Code Docker https://www.datacamp.com/tutorial/claude-code-docker
Skills Playground — Complete Docker Guide 2026 https://skillsplayground.com/guides/claude-code-docker/
Northflank — How to Sandbox AI Agents https://northflank.com/blog/how-to-sandbox-ai-agents
SoftwareSeni — Why Docker Is Not Enough https://www.softwareseni.com/ai-agent-sandboxing-explained-why-docker-is-not-enough-and-what-actually-works/
Bunnyshell — Sandboxed Environments for AI https://www.bunnyshell.com/guides/sandboxed-environments-ai-coding/
Coding Agent Sandbox List (wincent gist) https://gist.github.com/wincent/2752d8d97727577050c043e4ff9e386e
awesome-sandbox https://github.com/restyler/awesome-sandbox
awesome-AI-sandbox https://github.com/webcoyote/awesome-AI-sandbox
bureado/awesome-agent-runtime-security https://github.com/bureado/awesome-agent-runtime-security
gVisor Architecture https://gvisor.dev/docs/architecture_guide/intro/
Firecracker MicroVM https://github.com/firecracker-microvm/firecracker
microsandbox https://github.com/microsandbox/microsandbox
container-use (Dagger) https://github.com/dagger/container-use
E2B https://e2b.dev
Kata Containers https://github.com/kata-containers/kata-containers
HN: Docker container for Claude Code bypass https://news.ycombinator.com/item?id=44956002
dagger/container-use https://github.com/dagger/container-use
Anthropic Trust Center https://trust.anthropic.com
Claude Code Permission Modes https://code.claude.com/docs/en/permission-modes
Managed Settings Reference https://code.claude.com/docs/en/settings

Last updated: May 2026. The container-based Claude Code ecosystem is evolving rapidly — check the Anthropic devcontainer docs and the sandbox gists above for the latest updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment