What you'll have when done: Claude Code running fully inside a Docker container, using
--dangerously-skip-permissionsfreely, 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.
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.
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:
You should see "Hello from Docker!"
docker run --rm hello-world
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
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.)
- Claude Pro / Max / Teams / Enterprise subscription → you'll use the
setup-tokenmethod (recommended, no usage charges beyond subscription) - Anthropic API key from console.anthropic.com → you'll use
ANTHROPIC_API_KEY(pay-per-token billing)
This is the credential that goes into every container without you having to log in interactively each time.
Run this on your host machine:
claude setup-tokenIt 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
export ANTHROPIC_API_KEY="sk-ant-api03-..." # Linux/macOS
$env:ANTHROPIC_API_KEY = "sk-ant-api03-..." # Windows PowerShellWhy not just mount
~/.claudedirectly? The~/.claudedirectory 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.
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 .devcontainerFinal layout we're building:
~/claude-sandbox/
├── .devcontainer/
│ ├── Dockerfile
│ ├── devcontainer.json # (Path B only)
│ └── init-firewall.sh
└── run-claude.sh # (Path A launcher script)
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"]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" -NoNewlineOr install dos2unix and run
dos2unix .devcontainer/init-firewall.shin WSL.
# 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"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.
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"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| 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 |
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.shYou 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 logininside 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.
While Claude is running, open a second terminal and confirm the sandbox boundaries.
# 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.
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.
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.
# Windows — go to your project and run
cd C:\projects\my-app
& "$env:USERPROFILE\claude-sandbox\run-claude.ps1"The named volume claude-config-my-app stores:
- Your auth credentials (no re-login needed)
- Session history
- Your
CLAUDE.mdfiles 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.
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.
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.
# Mount your custom slash commands read-only:
-v "$env:USERPROFILE\.claude\commands:/home/node/.claude/commands:ro"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.
docker volume rm claude-config-my-appThe next run creates a fresh volume. You'll need to re-authenticate once.
docker volume ls | Select-String "claude-config"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.
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 the project folder in VS Code
- If prompted "Reopen in Container" — click it
- Or: press
Ctrl+Shift+P→ type "Dev Containers: Reopen in Container" → Enter - Wait for the build (first time: 3-5 minutes)
- Open the integrated terminal — you're inside the container
- Run:
claude --dangerously-skip-permissions
Ctrl+Shift+P → "Dev Containers: Rebuild Container"
The volumes (~/.claude) are preserved. Only the container image is rebuilt.
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/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.
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
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.
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 -LIf 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-isolatedThis cuts off all internet access. Add --network bridge back if you need outbound, but then egress filtering requires a different approach (external proxy).
This is expected and harmless. The container intentionally blocks self-update (DISABLE_AUTOUPDATER=1). Rebuild the image to update instead.
Tokens from claude setup-token are valid for one year. If expired, re-run it on the host:
claude setup-tokenUpdate CLAUDE_CODE_OAUTH_TOKEN in your shell profile and restart your terminal.
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.
Docker Desktop isn't running. Open it from the Start menu and wait for the whale icon to stabilize.
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/"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.
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
~/.claudeas 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:
- gVisor runtime — user-space kernel, replace
--runtime=runcwith--runtime=runsc - trailofbits/claude-code-devcontainer — hardened template built specifically for security audits
- Firecracker MicroVMs via E2B — hardware-level isolation via SaaS
# 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