
Claude Code CLI can run bash commands, install packages, and make network requests. That's powerful, but it also means a single bad prompt could reach places you don't want it to. I wanted to run Claude Code in an environment where it has full autonomy over the filesystem but can only talk to a handful of approved domains.

Here's how I set it up with Docker and a Squid proxy.

## What We're Building

The setup has two pieces:

1. A **Squid proxy** that maintains a domain allowlist. Only traffic to approved domains gets through.
2. A **Claude Code container** that sits on an internal Docker network with no direct internet access. All its traffic goes through the proxy.

```
┌─────────────────────────────────────────────┐
│  Host                                       │
│                                             │
│  ┌────────────┐    ┌─────────────────────┐  │
│  │ Squid      │◄───│ isolated network    │  │
│  │ (allowlist)│    │   ┌───────────────┐ │  │
│  └─────┬──────┘    │   │ Claude Agent  │ │  │
│        │           │   │ (ephemeral)   │ │  │
│   internet         │   └───────────────┘ │  │
│   network          └─────────────────────┘  │
│        │                                    │
│        ▼                                    │
│   Internet (filtered)                       │
└─────────────────────────────────────────────┘
```

The agent can do whatever it wants inside `/workspace`. But if it tries to reach `example.com` or any domain not on the list, the proxy blocks it.

## The Files

You need four files. Here's each one.

### Dockerfile

```dockerfile
FROM node:20-slim

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

RUN apt-get update && apt-get install -y git curl ca-certificates && rm -rf /var/lib/apt/lists/*

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

USER node
WORKDIR /workspace

ENTRYPOINT ["entrypoint.sh"]
```

The `node` user in `node:20-slim` has UID 1000, which matches most Linux host users. This makes volume mount permissions work without extra configuration.

### entrypoint.sh

Claude Code reads OAuth credentials from `~/.claude` and `~/.claude.json`. We mount the host's credentials as read-only, then copy them into the container so Claude has a writable home directory for session data.

```bash
#!/bin/bash
set -e

if [ -d "/host-claude" ]; then
    mkdir -p "$HOME/.claude"
    if [ -f "/host-claude/.credentials.json" ]; then
        cp "/host-claude/.credentials.json" "$HOME/.claude/.credentials.json"
    fi
    for f in statsig.json settings.json; do
        if [ -f "/host-claude/$f" ]; then
            cp "/host-claude/$f" "$HOME/.claude/$f"
        fi
    done
fi

if [ -f "/host-claude.json" ]; then
    cp "/host-claude.json" "$HOME/.claude.json"
fi

exec claude "$@"
```

### squid.conf

This is the allowlist. Edit it to match what your agent needs access to.

```squid
acl allowed_sites dstdomain .anthropic.com
acl allowed_sites dstdomain .github.com
acl allowed_sites dstdomain .googleapis.com
acl allowed_sites dstdomain registry.npmjs.org
acl allowed_sites dstdomain .sentry.io
acl allowed_sites dstdomain .statsigapi.net

acl SSL_ports port 443
acl CONNECT method CONNECT
http_access allow CONNECT allowed_sites
http_access allow allowed_sites
http_access deny all

http_port 3128
```

One thing to watch out for: Squid doesn't allow overlapping entries. If you add `.anthropic.com` (with the leading dot, which covers all subdomains), don't also add `api.anthropic.com`. Squid will refuse to start.

### docker-compose.yml

```yaml
services:
  proxy:
    image: ubuntu/squid
    volumes:
      - ./squid.conf:/etc/squid/squid.conf:ro
    networks:
      - isolated
      - internet
    restart: unless-stopped

  agent:
    build: .
    depends_on:
      - proxy
    environment:
      - HTTP_PROXY=http://proxy:3128
      - HTTPS_PROXY=http://proxy:3128
      - http_proxy=http://proxy:3128
      - https_proxy=http://proxy:3128
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
    volumes:
      - ${WORKSPACE:-./workspace}:/workspace
      - ${HOME}/.claude:/host-claude:ro
      - ${HOME}/.claude.json:/host-claude.json:ro
    networks:
      - isolated
    deploy:
      resources:
        limits:
          cpus: "${AGENT_CPUS:-2}"
          memory: "${AGENT_MEMORY:-4g}"
    profiles:
      - run

networks:
  isolated:
    internal: true
  internet:
    driver: bridge
```

The `agent` service is under the `run` profile, so `docker compose up` only starts the proxy. Agents are started on demand with `docker compose run`.

## Usage

Start the proxy once:

```bash
docker compose up -d proxy
```

Run Claude Code against a project:

```bash
WORKSPACE=/path/to/project docker compose run --rm agent \
  -p "Your prompt here" --dangerously-skip-permissions
```

Everything after `agent` is passed directly to the Claude CLI. You get access to every flag Claude supports without any wrapper getting in the way.

When you're done:

```bash
docker compose down
```

### Skipping Permissions

The `--dangerously-skip-permissions` flag tells Claude Code to execute all tool calls without asking for confirmation. Normally you wouldn't want this, but inside an isolated container with filtered network access, it makes sense. The container is the sandbox.

### Parallel Agents

Each `docker compose run` creates a separate container. Run as many as you need:

```bash
WORKSPACE=./project-a docker compose run --rm agent -p "Fix tests" --dangerously-skip-permissions &
WORKSPACE=./project-b docker compose run --rm agent -p "Add logging" --dangerously-skip-permissions &
wait
```

They share the proxy but are otherwise completely isolated from each other.

### API Key Instead of OAuth

If you prefer API key authentication over OAuth:

```bash
ANTHROPIC_API_KEY=sk-ant-xxx docker compose run --rm agent \
  -p "Your prompt" --dangerously-skip-permissions
```

### Resource Limits

Default is 2 CPUs and 4GB memory. Override with environment variables:

```bash
AGENT_CPUS=4 AGENT_MEMORY=8g WORKSPACE=./project docker compose run --rm agent \
  -p "Heavy task" --dangerously-skip-permissions
```

## Optional: Wrapper Script

If you don't want to type `docker compose -f /path/to/docker-compose.yml` every time, a small wrapper script helps:

```bash
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export WORKSPACE="${WORKSPACE:-$(pwd)}"

case "${1:-help}" in
    up)     docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d proxy ;;
    down)   docker compose -f "$SCRIPT_DIR/docker-compose.yml" down ;;
    status) docker compose -f "$SCRIPT_DIR/docker-compose.yml" ps ;;
    run)    shift; docker compose -f "$SCRIPT_DIR/docker-compose.yml" run --rm agent "$@" ;;
    *)      echo "Usage: $0 {up|down|status|run [claude args...]}" ;;
esac
```

Or add a shell alias to your `~/.zshrc`:

```bash
alias claude-docker='docker compose -f ~/path/to/docker-compose.yml'
```

Then from anywhere:

```bash
claude-docker run --rm agent -p "Your prompt" --dangerously-skip-permissions
```

## Verifying the Isolation

To confirm the proxy is actually blocking traffic:

```bash
# This should fail (not on the allowlist)
docker compose run --rm agent curl -s https://example.com
# Connection refused

# This should succeed (on the allowlist)
docker compose run --rm agent curl -s -o /dev/null -w "%{http_code}" https://api.anthropic.com
# 404 (reachable, just no content at the root)
```

## Wrapping Up

This setup gives you the best of both worlds. Claude Code gets full autonomy to read files, write code, run tests, and install packages. But it can only talk to the domains you approve. The Squid proxy handles the filtering, Docker handles the isolation, and `docker compose run` handles spinning up ephemeral agents.

The whole thing is about 80 lines of configuration across four files. No custom tooling, no complex orchestration.
