Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

aisandbox — AI monolith dev sandbox

An isolated all-in-one Docker container for development: PostgreSQL, MySQL, Valkey, Kafka (KRaft), NATS (JetStream), etcd, MinIO, nginx, Ollama, Go/Node/Python, Playwright for screenshots, and Claude Code inside. Your current project directory is mounted at /workspace; database state lives in Docker volumes and survives container recreation.

Runs on Linux (x86_64, including Fedora/SELinux) and macOS (Apple Silicon and Intel) — the image is multi-arch via ARG TARGETARCH, which BuildKit substitutes automatically during a plain docker build.

Why this exists

The core purpose is isolating an autonomous AI agent. Claude Code (and other agents) run commands and edit files on their own, often without confirming every step, and on top of that have passwordless sudo inside. Running that straight on your work machine is risky: one bad command (rm -rf, a "let me clean the cache", some sketchy script off the internet) hits the host. The container is a boundary the agent can't step outside of.

  • Filesystem. The agent only sees /workspace (the current project) — the rest of the host tree (~, other repos, system files) is out of reach. A mistake or hallucination damages, at worst, one project directory that's already under git, not your whole $HOME.
  • SSH keys and secrets. ~/.ssh, ~/.aws, ~/.config, ~/.kube, tokens in the host's environment are not passed into the container. The agent physically cannot read a private key and "just in case" push/deploy somewhere or leak it into a log. If something inside needs access, that's a separate, scoped key/token you add deliberately — not your entire keyring.
  • sudo without consequences. Inside, the sandbox user has passwordless sudo so the agent can apt-get a package without tripping. On the host that would be a hole; in a disposable container it isn't — root is confined to the container.
  • Network. Service ports aren't published (no -p), so databases/services the agent spins up don't sit exposed on your LAN and don't clash with your host Postgres/MySQL on the same ports.
  • No cruft on the host. Dozens of system packages, a JVM, Chromium, Ollama models, Kafka cluster data — all of it lives in the image and volumes instead of smearing across your working OS. Done with it? docker rm, and the host is clean.
  • Reproducibility. The same environment on a Fedora laptop and a Mac mini: same Go version, same services, same paths. "Works on my machine" stops depending on whatever accumulated locally.

The trade-off is honest: /workspace is a bind-mount of a real directory, so the agent writes for real inside the project (otherwise it's useless). The isolation protects everything around the project, not the project itself — so keep the project under git and don't mount anything extra into the sandbox (parent directories, the whole $HOME).

Data leaks — what the sandbox does NOT cover

The isolation above is about the agent not reading things it shouldn't and not wrecking the host. But by default the container has full outbound internet, and it reads /workspace for real. That leaves one class of risk the container doesn't remove on its own: exfiltration of what's mounted. The agent can read your code and send it anywhere — curl, git push, a call to some external API — by accident, from a bad instruction, or via prompt injection sitting in some file or dependency.

What to do about it:

  • Keep secrets out of /workspace. The agent sees .env, private keys and tokens that live in the project, and can leak them. Keep them in host environment variables (not passed into the container) or a separate store, not as a file in the repo. If you can't avoid .env, at least .gitignore it and keep only dev stubs there, never production keys.
  • Restrict outbound traffic when the task doesn't need it:
    • fully offline — run with --network none. Key point: internal services (Postgres, Kafka, MinIO, etc.) listen on localhost inside the container and are unaffected — loopback stays up. Only external internet drops: neither apt-get nor the agent can reach out. Good for agent runs after the image is already built.
    • domain allowlist — via an egress proxy/firewall: the agent only reaches the hosts it needs (api.anthropic.com, github.com, package mirrors), everything else is blocked. Docker has no built-in domain allowlist — that's a separate proxy or a custom bridge + iptables on the host.
  • Claude Code sends code to Anthropic's API by design — that's not a "leak", it's how it works, but keep in mind the contents of /workspace go to the model's server. The sandbox doesn't change that.

Bottom line: the container is great at containing the blast radius of destructive actions and hiding host-side secrets, but it does not guarantee that mounted code stays put. That's on egress control and secret hygiene, not on the mere fact of running in Docker.

Repository layout

aisandbox/
├── setup.sh        # installer: pick OS + user name, copies files, sets PATH
├── Dockerfile      # image (Ubuntu 26.04, multi-arch amd64/arm64)
├── entrypoint.sh   # starts services, then drops to the sandbox-user shell
└── aisandbox       # launcher: builds the image + runs a per-project container

Installation

0. Docker (prerequisite)

Linux (Fedora):

sudo dnf install docker docker-buildx        # or docker-ce from Docker's repo
sudo systemctl enable --now docker
sudo usermod -aG docker $USER && newgrp docker

BuildKit is required (enabled by default in current Docker; otherwise export DOCKER_BUILDKIT=1). It's mandatory because the Dockerfile uses a heredoc (COPY <<EOF) and ARG TARGETARCH.

macOS:

brew install --cask docker    # Docker Desktop
# or: brew install colima docker && colima start --cpu 4 --memory 8 --disk 60

In Docker Desktop, allocate resources: at least 4 CPU / 8 GB RAM (Settings → Resources). Kafka + MySQL + Ollama together are hungry; on a Mac mini M4 16 GB set 8 GB (10 max) so the host keeps some headroom.

1. Run the installer

From the folder with these files:

chmod +x setup.sh
./setup.sh

It detects your OS (Linux/macOS), asks for the in-container user name (defaults to your host user), then copies Dockerfile + entrypoint.sh into ~/aisandbox, installs the aisandbox launcher into ~/.local/bin, adds that to your PATH in the right rc file (.bashrc on Linux, .zshrc on macOS), and offers to build the image.

Non-interactive / flags:

./setup.sh --os linux  --user dev  --build           # do everything, build now
./setup.sh --os macos  --user dev  --no-build        # install only, build later
./setup.sh -y                                        # accept all defaults
./setup.sh --help

The chosen user name is written to ~/aisandbox/aisandbox.conf and passed into the image as --build-arg SANDBOX_USER=… on every build. To change it later, edit that file (or re-run setup.sh) and rebuild.

After install, open a new shell (or source ~/.bashrc / source ~/.zshrc) so aisandbox is on PATH.

SANDBOX_HOME overrides where the files go (default ~/aisandbox).

2. Build the image (if you skipped it in the installer)

aisandbox build              # builds the aisandbox image from $SANDBOX_HOME
aisandbox build --no-cache   # full rebuild

You don't have to build manually either: on the first aisandbox run the launcher builds the image itself if it's missing.

About UID/GID (matters on Linux): the launcher passes --build-arg HOST_UID=$(id -u) HOST_GID=$(id -g) automatically — files the container creates in /workspace will be owned by you, not by root. On macOS nothing needs to be passed: Docker Desktop's virtiofs maps ownership automatically, so the image is built with the default UID 1000 (macOS's native GID 20 is taken by the dialout group in Ubuntu, which is why it isn't used).

The build isn't fast (Valkey is compiled from source, Playwright downloads Chromium) — roughly 10–15 minutes on an M4, cached afterwards.

Usage

cd ~/projects/myproject
aisandbox                    # sandbox for the current directory

aisandbox ~/projects/myproject2   # or point to a directory explicitly

The first run creates a container named aisandbox-<dir-name>; subsequent runs reconnect to it (docker start -ai), so packages installed inside and database data persist. Exit with exit or Ctrl-D (the container stops but is not removed).

Everything runs on localhost inside the container:

Service Port Notes
PostgreSQL 5432
MySQL 3306 initialized with no root password
Valkey/Redis 6379
Kafka (KRaft) 9092
NATS 4222 JetStream enabled
etcd 2379
MinIO 9000 / 9001 (UI) admin / adminadmin
Ollama 11434 models in a shared volume

PostgreSQL, MySQL, MinIO data and Ollama models live in shared ai-monolith-* volumes — they are shared across all projects and survive docker rm of the container.

Screenshots for layout verification (inside the container):

python3 /usr/local/bin/capture.py http://localhost:3000 --w 1920 --h 1080 --out desktop.png
python3 /usr/local/bin/capture.py http://localhost:3000 --w 390  --h 844  --out mobile.png

Maintenance

# remove a project's container (volume data survives)
docker rm -f aisandbox-myproject

# remove everything, including database data
docker rm -f $(docker ps -aq --filter name=aisandbox-)
docker volume rm ai-monolith-postgresql ai-monolith-mysql ai-monolith-ollama ai-monolith-minio

# update entrypoint.sh — no rebuild needed: it's bind-mounted from the host,
# just edit ~/aisandbox/entrypoint.sh and restart the container

Platform notes

Fedora/SELinux: the launcher adds :z to mounts automatically when getenforce reports something other than Disabled. Nothing to do.

macOS:

  • /workspace bind-mount I/O is slower than native (virtiofs). Heavy Go builds are fine; if it drags, keep GOCACHE inside the container rather than under /workspace.
  • ai-monolith-* volumes live inside the Docker Desktop VM — leave disk headroom (Settings → Resources → Disk, 60+ GB, especially because of Ollama models).
  • Ollama inside the container on Apple Silicon runs CPU-only — the GPU (Metal) is not passed through to the Linux VM. For accelerated inference on an M4, prefer a native Ollama on the host (brew install ollama) and reach it from the container at http://host.docker.internal:11434.
  • Service ports are not published to the host (there's no -p) — you can't reach the container's databases directly from the host; work happens inside the container. If you need host access, add -p 5432:5432 etc. to aisandbox at container-creation time.

Switching architectures: an image built on x86 won't run on an M4 (and vice versa) — build a local one on each machine (aisandbox build). MySQL/PostgreSQL data volumes are portable across architectures, but it's better not to move them — keep them local to each machine.

Why not just run the agent on the host?

Because an autonomous agent that executes commands is one bad step away from wrecking the machine it runs on — a wrong rm, a destructive DB migration, a script it pulled off the internet, or a prompt injection that turns its own tools against you. It doesn't take a "malicious" model; a confident wrong action is enough. And models do go off the rails in practice — looping, ignoring instructions, taking drastic actions no one asked for.

External video, cases of AI models going off the rails: https://www.youtube.com/watch?v=7kWkUoR2bg0

The sandbox doesn't make the agent smarter or better-behaved — it makes "off the rails" survivable. When the worst happens, the blast radius is one container plus one project directory that's already under git; your host filesystem, SSH keys and the rest of $HOME stay out of reach, and cleanup is a single docker rm. That's the whole point of not handing an autonomous tool your real shell.