This devcontainer uses a Squid proxy sidecar to restrict network access. The Claude container runs on an internal Docker network with no direct internet access - all HTTP/HTTPS traffic must go through the proxy.
- Cross-platform - Works on Linux and macOS (Docker Desktop)
- Corporate proxy support - Auto-detects and chains to upstream proxy
- Defense-in-depth - Squid allowlist + optional iptables hardening in proxy
- No capabilities on Claude container - NET_ADMIN/NET_RAW only on proxy
- Docker (Docker Desktop on macOS/Windows, or Docker Engine on Linux)
- Docker Compose v2
- No special host permissions required
┌─────────────────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ claude-code │ │ proxy │ │
│ │ │ ──────► │ (Squid) │ ──► Corporate ──► Internet
│ │ Internal │ HTTP │ │ Proxy
│ │ Network Only │ HTTPS │ NET_ADMIN │ (optional)
│ │ │ │ NET_RAW │ │
│ │ NO caps │ │ + iptables │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ ─────┴───────────────────────────┴───── │
│ Internal Network (no internet) │
└─────────────────────────────────────────────────────────────────────┘
- Open this folder in VS Code
- When prompted, click "Reopen in Container"
- Docker Compose will start both containers automatically
# Start containers
docker compose up -d
# Enter the Claude container
docker compose exec claude-code zsh
# View proxy logs
docker compose logs -f proxy
# Stop everything
docker compose downWhen running claude for the first time, you'll need to authenticate via OAuth. Due to network restrictions, the browser may not be able to reach the callback URL automatically.
Workaround: When the login URL appears in the terminal, manually copy and open it in your browser. After authenticating in the browser, you can then paste in the provided token.
If your environment uses a corporate proxy, the proxy container will automatically detect and chain to it.
Set one of these environment variables on the host before running docker compose up:
HTTP_PROXYhttp_proxy
The value is passed to the proxy container's UPSTREAM_PROXY variable.
Method 1: URL with embedded credentials
export HTTP_PROXY="http://user:password@corporate-proxy.example.com:8080"
docker compose up -dMethod 2: Separate credentials
export HTTP_PROXY="http://corporate-proxy.example.com:8080"
export UPSTREAM_PROXY_USER="myuser"
export UPSTREAM_PROXY_PASS="mypassword"
docker compose up -dMethod 3: Using .env file
Create a .env file in this directory:
HTTP_PROXY=http://corporate-proxy.example.com:8080
UPSTREAM_PROXY_USER=myuser
UPSTREAM_PROXY_PASS=mypassword
docker compose logs proxy | grep -i upstream
# Should show: "Detected upstream proxy: http://..."
# And: "Configuring proxy chaining to corporate-proxy:8080"To prevent exfiltration of your Anthropic API key, you can configure the proxy to inject it rather than having it in the Claude container.
# Set your API key on the host (it will only be passed to the proxy container)
export ANTHROPIC_API_KEY="sk-ant-..."
docker compose up -dOr use a .env file:
ANTHROPIC_API_KEY=sk-ant-...
┌─────────────────┐ ┌─────────────────┐
│ claude-code │ ───► │ proxy │ ───► api.anthropic.com
│ │ (no │ :3129 │ (with
│ NO API KEY │ key) │ injects key │ key)
└─────────────────┘ └─────────────────┘
- Claude Code sends API requests to
http://proxy:3129(viaANTHROPIC_BASE_URL) - The proxy intercepts requests and adds the
x-api-keyheader - Request is forwarded to
api.anthropic.comwith the key - The API key never exists in the Claude container's environment or filesystem
# Check proxy has the key configured
docker compose logs proxy | grep -i "anthropic-proxy"
# Should show: "API key injection enabled: sk-ant-..."
# Verify Claude container does NOT have the key
docker compose exec claude-code env | grep -i anthropic
# Should show ANTHROPIC_BASE_URL but NOT ANTHROPIC_API_KEYIf ANTHROPIC_API_KEY is not set, the proxy passes requests through without modification. This allows:
- OAuth login to work normally
- Using keys configured inside the container (less secure)
- Claude container is on an
internal: trueDocker network - No default gateway - cannot reach internet directly
- Can only communicate with proxy container
- Squid proxy only allows domains in
.proxy/allowlist.txt - All other domains are blocked with HTTP 403
- Proxy container has NET_ADMIN/NET_RAW capabilities
- On startup, runs
init-firewall.sh:- Resolves allowlist domains to IPs
- Creates ipset with allowed IPs
- Configures iptables to DROP non-allowed traffic
- Even if Squid is somehow bypassed, traffic is still filtered
- API key lives only in the proxy container, never in Claude container
- Anthropic API requests route through a dedicated proxy (port 3129)
- The proxy injects the
x-api-keyheader before forwarding to Anthropic - Even if malicious code runs in Claude container, it cannot access the API key
Inside the Claude container:
# These should WORK
curl -I https://api.github.com
curl -I https://registry.npmjs.org
curl -I https://api.anthropic.com
npm install express
pip install requests
# These should FAIL (blocked by proxy)
curl -I https://example.com
curl -I https://google.com
# Direct connections should also FAIL (no route)
curl --noproxy '*' https://google.comEdit .proxy/allowlist.txt to modify the allowlist.
Current allowlist:
- GitHub (*.github.com, *.githubusercontent.com, *.githubassets.com)
- NPM Registry (*.npmjs.org, *.npmjs.com)
- PyPI (*.pypi.org, *.pythonhosted.org)
- VS Code (*.visualstudio.com, vscode.blob.core.windows.net, *.code.visualstudio.com, *.vo.msecnd.net)
- Anthropic (*.anthropic.com, claude.ai, *.claude.com)
- Sentry (sentry.io only, no subdomains)
- Statsig (statsig.anthropic.com, *.statsig.com)
After editing, rebuild the proxy:
docker compose build proxy
docker compose up -d proxyBy default, git is configured to use HTTPS instead of SSH:
git config --global url."https://github.com/".insteadOf git@github.com:This ensures git operations go through the proxy.
If you need SSH git operations, the container is configured to tunnel SSH through the proxy using socat. This requires the proxy to allow CONNECT to port 22 (already configured).
View what traffic is being allowed/blocked:
# Follow logs
docker compose logs -f proxy
# Or exec into proxy container
docker compose exec proxy tail -f /var/log/squid/access.logEnsure the proxy container is running and healthy:
docker compose ps
docker compose logs proxyCheck environment variables are set:
env | grep -i proxyShould show HTTP_PROXY, HTTPS_PROXY, etc.
Check proxy logs for authentication errors:
docker compose logs proxy | grep -i authEnsure credentials don't contain special characters that need URL encoding.
Check if NET_ADMIN capability is granted:
docker compose exec proxy capsh --print | grep cap_net_adminCheck firewall script output:
docker compose logs proxy | grep -i firewallEdit .proxy/allowlist.txt:
# My custom domain
.mycustomdomain.com
Then rebuild:
docker compose build proxy && docker compose up -d proxyRemove the cap_add section from docker-compose.yml:
proxy:
# cap_add:
# - NET_ADMIN
# - NET_RAWTo use your existing Claude configuration, edit docker-compose.yml:
claude-code:
volumes:
- ${HOME}/.claude:/home/node/.claude:cached- DNS queries can still reach the internet (via Docker's DNS)
- Data could theoretically be exfiltrated via DNS tunneling
- The proxy logs all requests - review periodically
- The VS Code blob storage domain (
vscode.blob.core.windows.net) is an Azure CDN endpoint - With credential isolation enabled: API key cannot be exfiltrated as it never enters the Claude container
- Without credential isolation: Credentials could be exfiltrated via allowed domains (e.g., creating a GitHub gist)