Containerized AI coding agents with network isolation. Runs Claude Code or OpenCode in Docker with a Squid proxy that restricts internet access to an allowlisted set of domains.
Add these to your ~/.zshrc (or ~/.bashrc) to run agents from anywhere:
# dev-agent: run AI coding agents from anywhere
ai() {
/path/to/dev-agent/run.sh "$@"
}
ai-open() {
/path/to/dev-agent/run.sh --opencode "$@"
}Then source ~/.zshrc or open a new terminal.
Supports two authentication modes, auto-detected from your host's ~/.claude/settings.json:
- Bedrock — if
CLAUDE_CODE_USE_BEDROCKis present in theenvsection (requires AWS credentials) - Max — otherwise (uses your Claude Max subscription; run
claude logininside the container on first use)
# Using the shell function
ai ~/projects/my-app
# Build images first (required on first run or after Dockerfile changes)
ai --build ~/projects/my-app
# Or call run.sh directly
./run.sh ~/projects/my-appRequires an OPENROUTER_API_KEY environment variable.
# Using the shell function
OPENROUTER_API_KEY=sk-or-... ai-open --build ~/projects/my-app
# Or export the key once per session / in your shell config
export OPENROUTER_API_KEY=sk-or-...
ai-open ~/projects/my-apprun.sh orchestrator (picks agent via flag)
├── proxy (always starts) squid proxy with domain allowlisting
├── agent (--profile claude) Claude Code via AWS Bedrock
└── opencode (--profile opencode) OpenCode via OpenRouter
Each agent container starts with a firewall (iptables) that blocks all direct internet access. The only path to the outside world is through the Squid proxy, which only permits requests to domains listed in allowed-domains.txt. Traffic between containers on the Docker network is unrestricted.
The proxy is always running. Only one agent runs at a time, selected by Docker Compose profiles.
./run.sh [options] [project-dir]
| Option | Description |
|---|---|
project-dir |
Path to mount as /workspace (default: current directory) |
--opencode |
Use OpenCode instead of Claude Code |
--build |
Force rebuild Docker images |
--shell |
Start a bash shell instead of the agent |
--down |
Stop and remove containers |
--reload-proxy |
Hot-reload Squid config without restarting |
-h, --help |
Show help |
Options can be combined: ./run.sh --opencode --build --shell ~/projects/my-app
Each workspace gets its own isolated set of containers, named dev-agent-<dirname>. You can run multiple agents simultaneously against different projects:
# Terminal 1
./run.sh ~/projects/frontend
# Terminal 2
./run.sh ~/projects/backendThese are fully independent — separate proxy, separate agent, separate network.
Exiting the agent (or the shell) automatically tears down all containers for that instance. To force stop:
# Stop a specific instance (run from the same project directory)
./run.sh --down
# Or with the project path
./run.sh --down ~/projects/my-app
# For OpenCode instances, include the flag so the correct profile is stopped
./run.sh --opencode --down ~/projects/my-appThe --shell flag drops you into bash inside the agent container. The agent binary is on PATH, so you can launch it manually or poke around first:
./run.sh --shell ~/projects/my-app
# Inside container:
claude # launch Claude Code
opencode # (only available in --opencode containers)
curl --proxy http://proxy:3128 https://api.github.com/zen # test proxy
curl https://example.com # blocked by firewallProjects can customize their dev-agent environment by adding a .dev-agent/ directory to the project root. Both files are optional.
Requires yq installed on the host.
# Additional domains the agent can reach (merged with the default allowlist)
allowed_domains:
- .internal-registry.example.com
- api.my-saas.io
# Environment variables passed to the agent container
env:
DATABASE_URL: "postgres://postgres:postgres@db:5432/myapp"
REDIS_URL: "redis://redis:6379"
# Extra volume mounts (host:container[:mode])
# Relative paths resolve from the workspace root
volumes:
- ./data:/data:rw
- ../shared-lib:/shared:ro
# Attach to existing external Docker networks (must already be running)
networks:
- myapp_defaultHow merging works: run.sh parses this file and generates temporary Docker Compose override files in /tmp/dev-agent-<dirname>/. Domain lists are concatenated with the base allowlist. Environment variables and volumes are injected into the active agent service. Everything is cleaned up on exit.
Standard Docker Compose format. Services defined here run alongside the agent on the same network and are reachable by hostname.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:With this in place, the agent can reach db:5432 and redis:6379 directly — no proxy needed for container-to-container traffic.
An example configuration is included in the example.dev-agent/ directory. Copy it to your project and customize:
cp -r /path/to/dev-agent/example.dev-agent ~/projects/my-app/.dev-agentThe proxy permits traffic to domains listed in allowed-domains.txt:
| Domain | Purpose |
|---|---|
.amazonaws.com, .awsapps.com |
AWS Bedrock, STS, SSO |
.github.com, .githubusercontent.com |
GitHub API and content |
registry.npmjs.org |
npm packages |
.anthropic.com |
Anthropic API and telemetry |
.claude.com |
Claude platform (Max subscription auth) |
sentry.io, statsig.com |
Error reporting and telemetry |
.openrouter.ai |
OpenCode provider (OpenRouter) |
.opencode.ai |
OpenCode |
Edit allowed-domains.txt to change the base allowlist. Use .example.com (leading dot) to match all subdomains. Changes can be applied without restarting:
# Edit allowed-domains.txt, then:
./run.sh --reload-proxyEach agent container runs an iptables firewall on startup (via init-firewall.sh) that:
- Allows loopback traffic
- Allows DNS (UDP 53) for Docker service discovery
- Allows traffic to all attached Docker bridge subnets (proxy, sidecar services, external networks)
- Allows established/related return traffic
- Rejects everything else (with ICMP feedback, not silent drop)
This means the agent cannot bypass the proxy by making direct HTTP requests to the internet, even though it has the proxy environment variables set. The firewall is the enforcement layer; the proxy vars are just configuration hints.
├── run.sh Main entry point / orchestrator
├── docker-compose.yml Service definitions (proxy, agent, opencode)
├── agent.Dockerfile Claude Code container image
├── opencode.Dockerfile OpenCode container image
├── proxy.Dockerfile Squid proxy container image
├── squid.conf Squid proxy configuration
├── allowed-domains.txt Base domain allowlist
├── entrypoint.sh Container entrypoint (runs firewall, then agent)
├── init-firewall.sh iptables firewall setup script
├── dot-claude/ Claude Code settings mounted into container
│ ├── settings.json Claude settings for Bedrock auth
│ └── settings.max.json Claude settings for Max subscription auth
├── dot-opencode/ OpenCode settings mounted into container
│ └── opencode.json OpenCode global config (provider, models)
└── example.dev-agent/ Example workspace config for reference
├── config.yml
└── docker-compose.override.yml
# Rebuild everything
./run.sh --build ~/projects/my-app
# Rebuild only the agent image
docker compose -p dev-agent-my-app --profile claude build agent
# Rebuild only the proxy
docker compose -p dev-agent-my-app --profile claude build proxyBoth agent containers include the same development tools:
- Runtime: Node.js 24, Python 3, .NET 8 + 10, PowerShell 7.4
- Build tools: make, dotnet-ef
- Version control: git, gh (GitHub CLI)
- Search: ripgrep
- AWS: AWS CLI v2
- Utilities: jq, curl, vim, less, zip/unzip, openssh-client
# View proxy access logs (from host, while containers are running)
docker compose -p dev-agent-my-app --profile claude exec proxy cat /var/log/squid/access.log
# Watch logs in real time
docker compose -p dev-agent-my-app --profile claude exec proxy tail -f /var/log/squid/access.log
# Test if a domain is allowed (from inside the agent container)
curl --proxy http://proxy:3128 https://some-domain.comEdit dot-claude/settings.json to change Claude Code's global configuration. This file is mounted read-only into the container at /home/node/.claude/settings.json. Key settings:
model— Default model identifierenv.ANTHROPIC_MODEL/env.ANTHROPIC_SMALL_FAST_MODEL— Bedrock model ARNsenv.AWS_PROFILE— AWS profile for Bedrock authpermissions.defaultMode— Permission mode (bypassPermissionsfor autonomous operation)
Edit dot-opencode/opencode.json to change OpenCode's global configuration. Mounted at /home/node/.config/opencode/opencode.json. The OPENROUTER_API_KEY environment variable is passed through from the host.
run.sh automatically detects when the workspace is a git worktree (.git is a file, not a directory) and mounts the main .git directory at the correct absolute path so that git operations work correctly inside the container.