A Docker-based development environment for Windows and Linux that provides isolated, SSH-accessible workspaces for Claude Code. Each sandbox runs in its own container with a restrictive network gateway that only allows traffic to Anthropic services and explicitly whitelisted services, keeping your development environment secure by default.
- File system — Only your project directory is bind-mounted into the container. Claude can read and write files there but has no access to the rest of your host filesystem.
- Networking — All outbound traffic is routed through an iptables gateway with DNS-based filtering. Only Anthropic domains necessary for Claude Code and specifically specified domains whitelisted for the development environment are allowed.
- Remote access — Connect from anywhere with your phone or other device.
- Session management - Uses tmux for session management, so reconnect and resume where you left off.
- Sandbox picker — A single SSH entry point that presents all available sandboxes.
- Multiple environments — Pre-built configurations for working with .NET, Node.js, and PHP. Add new ones with a minimal
compose.ymland a couple setup scripts.
NOTE: These instances should not be directly exposed to the public internet. See Remote Access for connecting from other devices.
SSH (port 22000) ┌─────────────────────────────┐
──────────────────────► │ Picker │
└────────────┬────────────────┘
SSH │
▼
SSH (port 22001-22999) ┌────────────────────────────┐
──────────────────────► │ Gateway │
│ (iptables + DNSmasq) │
└───┬─────────────────────┬──┘
SSH │ ▲ HTTPS │
▼ HTTPS │ ▼
┌─────────────┴──┐ Whitelisted
│ Claude Code │ services only
│ tmux │
└────────────────┘
Each project has two containers:
- Gateway -- iptables-based network gateway + DNSmasq DNS server. All outbound traffic from the sandbox is routed through this gateway, which only allows HTTPS connections to Anthropic-owned domains by default.
- Claude -- The development container. Runs SSH, tmux, and Claude Code. Your project directory is bind-mounted at
/workspace.
Additionally, there is a lightweight management container that discovers all running and stopped sandboxes via the Docker socket. It provides a single SSH entry point (port 22000) with an interactive menu for selecting and connecting to sandboxes. It's started automatically alongside any sandbox.
- Windows 10/11 with PowerShell 5.0+
- Docker Desktop installed and running
- SSH client (built-in on modern Windows)
- SSH key pair for authentication (see SSH Authentication)
-
Clone the repository:
git clone <repo-url> cd claude-sandbox
-
Run the installer to add the
claude-sandboxcommand to your PATH:.\install.ps1
This creates wrapper scripts in
~\.local\binand adds that directory to your user PATH. -
Restart your terminal so the PATH change takes effect.
claude-sandbox [-Start] [-Environment <name>] [-WorkDir <path>] [-SshPort <port>]
claude-sandbox -Restart [-Environment <name>] [-SshPort <port>]
claude-sandbox -Rebuild [-NoCache] [-Environment <name>] [-WorkDir <path>] [-SshPort <port>]
claude-sandbox -Connect [-Environment <name>]
claude-sandbox -Picker
claude-sandbox -CopySshKeys
claude-sandbox -AddFirewallRule [-Environment <name>]Commands (mutually exclusive, default: -Start):
| Command | Description |
|---|---|
-Start |
Start the sandbox (build if necessary) |
-Restart |
Stop and restart the container |
-Rebuild |
Force rebuild the container image |
-NoCache |
With -Rebuild, build without the Docker layer cache |
-Connect |
SSH into the container |
-Picker |
SSH into the sandbox picker |
-CopySshKeys |
Import SSH keys from ~/.ssh (see below) |
-AddFirewallRule |
Open SSH ports (sandbox + picker) in Windows Firewall (requests elevation) |
Options:
| Option | Description |
|---|---|
-Environment |
Runtime environment (inferred if only one exists for directory) |
-WorkDir |
Workspace directory (default: current directory) |
-SshPort |
SSH port on the host (default: auto-assigned, range 22001-22999) |
If you've previously launched a sandbox for a directory and there is only one environment associated with it, you can omit -Environment and it will be inferred automatically:
If multiple environments have been used with the same directory, you'll be prompted to specify which one.
Examples:
cd D:\dev\my-dotnet-solution
# Build a .NET sandbox
claude-sandbox -Environment dotnet
# Import public SSH keys from ~/.ssh
claude-sandbox -CopySshKeys
# Connect to a running sandbox
claude-sandbox -Connect
# Open Windows Firewall for remote access (requests elevation)
claude-sandbox -AddFirewallRule
# Restart a running sandbox
claude-sandbox -RestartPassword authentication is disabled. All sandbox containers share a single authorized keys file at ~\.claude-sandbox\authorized_keys. Add one public key per line. Changes take effect immediately on the next SSH connection -- no container restart required.
Option 1: Manual setup
Create the file and add your public key(s):
mkdir ~\.claude-sandbox -Force
copy ~/.ssh/id_ed25519.pub ~/.claude-sandbox/authorized_keysIf you don't have an SSH key pair, generate one first:
ssh-keygen -t ed25519Option 2: Import from ~/.ssh
The -CopySshKeys flag collects all public keys (~/.ssh/*.pub) and authorized keys (~/.ssh/authorized_keys) from your host into ~\.claude-sandbox\authorized_keys:
claude-sandbox -Environment base -CopySshKeysThis can be run at any time, including against already-running sandboxes, to refresh the keys.
Once the sandbox is running, the script outputs two connection options:
Connect directly to the sandbox:
ssh -p <port> claude@localhost
Connect through the sandbox picker:
ssh -p 22000 claude@localhost
The sandbox picker is started automatically alongside any sandbox. It listens on port 22000 and presents an interactive menu listing all running and stopped sandboxes. Selecting a stopped sandbox will start it automatically. This is especially useful for remote access, where you only need to remember one port.
To connect from another device (phone, tablet, laptop), first open the firewall:
claude-sandbox -Environment dotnet -AddFirewallRuleThis opens ports for both the sandbox and the picker (port 22000) in Windows Firewall.
Via the picker (recommended) -- Connect to the picker from any device and select a sandbox from the menu:
ssh -p 22000 claude@<dev-machine-ip>Direct connection -- Connect to a specific sandbox by port:
ssh -p <port> claude@<dev-machine-ip>SSH jump host -- If Docker ports are not directly reachable, chain through your dev machine using ProxyJump. This requires the OpenSSH server to be running on your Windows machine. To enable it (from an elevated PowerShell prompt):
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service sshd -StartupType AutomaticOn the connecting device, add to ~/.ssh/config:
Host sandbox
HostName localhost
Port 22000
User claude
ProxyJump <user>@<dev-machine-ip>
Then connect with ssh sandbox.
When you SSH in, you're automatically attached to a tmux session. On first connect, a Claude Code window is created. Windows persist across SSH disconnections, so you can reconnect and pick up where you left off.
Creating new windows:
| Method | Claude Code | Bash |
|---|---|---|
| Keyboard | Ctrl-b c |
Ctrl-b b |
| Status bar | Click c:+Claude |
Click b:+Bash |
Switching windows: Click a window tab in the status bar, or use Ctrl-b <number>.
Each combination of workspace path + environment name produces a stable instance:
- The instance name is derived from a hash, so running the same command again reconnects to the existing container.
- Per-instance state (Claude config, SSH host keys) is stored in
~\.claude-sandbox\<instance-name>\. - Multiple sandboxes can run concurrently on different ports.
To fully remove an instance, stop the container and delete its state directory:
docker compose -p <instance-name> down
Remove-Item -Recurse ~\.claude-sandbox\<instance-name>Plugins are supported by syncing them from your host machine's Claude Code installation. To use plugins in a sandbox:
- Install the plugins on your host (non-sandboxed) Claude Code instance first.
- When a sandbox starts, the entrypoint automatically copies plugin data from
~/.claude/plugins(mounted read-only) into the container. - Windows paths in plugin metadata are translated to Linux paths automatically.
No additional configuration is needed -- any plugins you have installed on the host will be available inside the sandbox.
Why sync instead of install natively? Most marketplace plugins are hosted on GitHub, which is blocked by the gateway's network rules by default. Copying pre-installed plugins from the host avoids the need to whitelist
github.com.
claude-sandbox -Environment dotnet- Image:
mcr.microsoft.com/dotnet/sdk:10.0 csharp-lslanguage server installed- Host NuGet cache (
~/.nuget/packages) is mounted for persistence
claude-sandbox -Environment php- Image:
php:8.4-cli - Extensions:
zip,intl,mbstring - Composer installed globally
- Host Composer cache (
~/.composer/cache) is mounted for persistence
claude-sandbox -Environment base- Image:
node:22-bookworm-slim - Node.js v22 pre-installed
- No additional setup beyond the shared tooling
-
Create a directory under
environments/:environments/my-env/ ├── compose.yml # Required: Docker Compose overrides ├── setup-root.sh # Optional: runs as root during image build └── setup-user.sh # Optional: runs as claude user during image build -
In
compose.yml, override the base image and add any volumes:services: claude: build: args: BASE_IMAGE: your-base-image:tag volumes: - "host-cache-path:/home/claude/.cache/tool"
-
Use
setup-root.shfor system packages (runs as root) andsetup-user.shfor user-level tools (runs as theclaudeuser). -
Launch it:
claude-sandbox -Environment my-env
The gateway container routes all traffic from the sandbox through iptables. Only connections to allowed domains are forwarded; everything else is dropped.
Base allowed domains are defined in gateway/allowed-domains.conf. To allow additional domains for a specific environment, create an allowed-domains.conf in the environment's folder and mount it in compose.yml:
environments/dotnet/allowed-domains.conf:
api.nuget.org
globalcdn.nuget.org
services:
gateway:
volumes:
- ${SANDBOX_ROOT}/environments/dotnet/allowed-domains.conf:/etc/gateway/allowed-domains.d/env.conf:roTo allow additional domains globally, add them to gateway/allowed-domains.conf.
Warning: Each domain you add expands the attack surface of the sandbox. An AI agent with network access could exfiltrate code, secrets, or conversation context to any allowed host. Only allow domains you trust and that the environment genuinely needs. Avoid broad wildcards or general-purpose hosts (e.g.
pastebin.com,github.com) unless you fully understand the risk.
claude-sandbox/
├── docker-compose.yml # Main orchestration (gateway + claude services)
├── dev.compose.yml # Dev overrides (bind-mounts runtime scripts)
├── run.ps1 # Entry point for launching sandboxes
├── install.ps1 # CLI installation script
├── gateway/
│ ├── Dockerfile # Alpine-based gateway image
│ ├── dnsmasq.conf # DNS configuration
│ ├── allowed-domains.conf # Base domain allowlist
│ └── start.sh # Gateway container entry point
├── shared/
│ ├── Dockerfile # Base image for all sandbox environments
│ ├── setup-root.sh # Root-level setup (packages, sshd, user creation)
│ ├── setup-user.sh # User-level setup (bashrc, Claude Code, tmux)
│ ├── config/
│ │ ├── tmux.conf # Tmux configuration (status bar, key bindings)
│ │ └── bashrc.append # Appended to ~/.bashrc at build time
│ └── runtime/
│ ├── init.sh # Container init (networking, drops to claude user)
│ ├── entrypoint.sh # Container startup (plugin sync, SSH, etc.)
│ ├── tmux-picker.sh # Tmux session attach/create on SSH connect
│ └── new-window.sh # Creates new tmux windows (used by status bar buttons)
├── picker/
│ ├── Dockerfile # Alpine-based picker image
│ ├── compose.yml # Picker container orchestration
│ ├── dev.compose.yml # Dev overrides for picker
│ ├── picker.sh # Interactive sandbox discovery and menu
│ └── entrypoint.sh # Injects picker SSH key into authorized_keys
└── environments/
├── dotnet/ # .NET 10.0 environment
│ ├── compose.yml
│ └── setup-user.sh
├── php/ # PHP 8.4 environment
│ ├── compose.yml
│ └── setup-root.sh
└── base/ # Node.js 22 environment
└── compose.yml
When working on the sandbox's own runtime scripts, use the hidden -SandboxDev flag to bind-mount them into the containers instead of using the copies baked into the images. This lets you edit scripts on the host and see changes immediately on the next connection or restart, without rebuilding.
claude-sandbox -Environment base -SandboxDevThis applies dev overrides to both the sandbox (dev.compose.yml) and the picker (picker/dev.compose.yml). Use -Rebuild to force a full rebuild of both.
Make sure Docker Desktop is running. Check logs with:
docker compose -p <instance-name> logsThe sandbox waits for sshd to be ready before returning, but if it times out:
- Verify the container is running:
docker ps - Check that the correct port is being used (shown in the launch output)
- Remove stale host keys:
ssh-keygen -R "[localhost]:<port>"
Verify that ~\.claude-sandbox\authorized_keys exists and contains your public key. You can re-import keys from ~/.ssh at any time:
claude-sandbox -CopySshKeysIf Claude Code can't reach Anthropic services, check the gateway logs:
docker compose -p <instance-name> logs gatewayTo allow additional domains for a specific environment, add an allowed-domains.conf to the environment's folder and mount it in its compose.yml. To allow them globally, add them to gateway/allowed-domains.conf. Either way, rebuild with -Rebuild.
Use the -Rebuild flag to force a fresh image build:
claude-sandbox -Environment base -RebuildDocker still uses the layer cache by default. To force a fully fresh build (e.g. to re-run the Claude Code installer or pick up upstream base-image changes), add -NoCache:
claude-sandbox -Environment base -Rebuild -NoCache