Skip to content

AndrewBenzSW/dev-agent

Repository files navigation

dev-agent

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.

Quick Start

Shell Functions

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.

Claude Code (default)

Supports two authentication modes, auto-detected from your host's ~/.claude/settings.json:

  • Bedrock — if CLAUDE_CODE_USE_BEDROCK is present in the env section (requires AWS credentials)
  • Max — otherwise (uses your Claude Max subscription; run claude login inside 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-app

OpenCode

Requires 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-app

How It Works

run.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.

CLI Reference

./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

Multi-Instance Support

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/backend

These are fully independent — separate proxy, separate agent, separate network.

Stopping Containers

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-app

Shell Access

The --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 firewall

Workspace Configuration

Projects can customize their dev-agent environment by adding a .dev-agent/ directory to the project root. Both files are optional.

.dev-agent/config.yml

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_default

How 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.

.dev-agent/docker-compose.override.yml

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-agent

Network Security Model

Domain Allowlisting

The 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-proxy

Firewall

Each agent container runs an iptables firewall on startup (via init-firewall.sh) that:

  1. Allows loopback traffic
  2. Allows DNS (UDP 53) for Docker service discovery
  3. Allows traffic to all attached Docker bridge subnets (proxy, sidecar services, external networks)
  4. Allows established/related return traffic
  5. 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.

Developing dev-agent

Project Structure

├── 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

Rebuilding After Changes

# 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 proxy

Agent Toolchain

Both 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

Proxy Debugging

# 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.com

Customizing Claude Code Settings

Edit 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 identifier
  • env.ANTHROPIC_MODEL / env.ANTHROPIC_SMALL_FAST_MODEL — Bedrock model ARNs
  • env.AWS_PROFILE — AWS profile for Bedrock auth
  • permissions.defaultMode — Permission mode (bypassPermissions for autonomous operation)

Customizing OpenCode Settings

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.

Git Worktree Support

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.

About

Container for running Claude in dangerously skip permissions mode

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors