Host-side ainb container
Subcommand drives docker compose directly against pinned HolyClaude image. Bundled template, pre-flight checks, status snapshot.
Ship Track A (host-side ainb container subcommand) within 1–2 weeks. Drive docker compose against pinned coderluii/holyclaude:1.2.2. Bundle the compose template. Pre-flight checks. Emit containers.status on the event bus so the future plugin is a swap, not a rewrite.
Open a feat/plugin issue for host/process/* host functions now. Track B (wasm plugin) is blocked on Phase 7+ until those land. This paper is the canonical consumer.
Users want claude / codex / copilot / gemini / cursor running as containers from ainb without the ainb team owning a Dockerfile or chasing upstream agent CLI changes. CoderLuii/HolyClaude already ships a pre-baked image bundling seven agents + a web UI (CloudCLI on :3001) to Docker Hub. The question is not whether to pull HolyClaude — it is how to wire it in.
HolyClaude has no CLI, no library, no SDK. The product is the image coderluii/holyclaude:1.2.2 + a docker-compose.yaml template. Integration = docker shell-outs + bind mounts + env vars.
Meanwhile feat/plugin defines a wasm32-wasip1 plugin model with strict capability gating. A spawn_subprocess capability is declared in the manifest schema — but no host/process/spawn host function exists yet. That single gap forces the option split.
Pull coderluii/holyclaude:1.2.2 · docker compose up -d · open http://localhost:3001. The product is the image, not a CLI. Here is the surface, grouped into what's in the box vs what ainb still has to handle.
siteboon/claudecodeui · GPL-3.0 (vendored tarball)PUID/PGID) — fixes bind-mount permssettings.json, CLAUDE.md, git identity~/.claude.json to host bind mount~/.codex, ~/.gemini, ~/.cursor configs from ./data/claudestop + error events~/.claude/notify-onANTHROPIC_AUTH_TOKEN=ollama + ANTHROPIC_BASE_URL=<endpoint>.env: HOLYCLAUDE_HOST_PORT, _CLAUDE_DIR, _WORKSPACE_DIRTZ, PUID, PGID, ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, CURSOR_API_KEY./data/claude/{settings.json,CLAUDE.md}:latest) · slim (:slim)./data/claude → /home/claude/.claude — creds + memory./workspace → /workspace — user codecloudcli-data → /home/claude/.cloudcli — CloudCLI SQLite:3001 exposed · dev ports 3000,5173,8787,9229,1455 commented outholyclaude CLI binarydocker + docker composedocker exec / attach wrapper commandsSYS_ADMIN + SYS_PTRACE + seccomp=unconfinedshm_size: 2g required (Chromium)sed/perl rewrites — break on every upstream bumpThree facts to get right before docker compose up: host and container have separate Claude accounts by default · OAuth happens inside the container, driven by your host browser hitting :3001 · all auth state lives in the ./data/claude bind mount, which is what makes credentials survive container restart.
HOLYCLAUDE_HOST_CLAUDE_DIR). This is the bind-mount source. Where the container's auth state actually lives, on your disk../data/claude/ — same bytes. Where the claude CLI inside the container looks for credentials, settings, memory../data/claude/.claude.json.persist by entrypoint.sh.Practical consequence: you can be logged into Max as one account on your host's Claude CLI, and a different Max account (or an API key, or Ollama) inside HolyClaude. They don't see each other unless you point the bind mount at host's ~/.claude — which you generally shouldn't, because the container runs as user claude (configurable via PUID/PGID) and UID mismatches will corrupt your host token store.
docker compose up -d · sentinel-gated bootstrap.sh runs once.http://localhost:3001. Create a CloudCLI account (10s, local SQLite, container-local).claude CLI inside the container. CLI prints OAuth URL + waits for paste-back code.claude CLI exchanges code → writes /home/claude/.claude/.credentials.json../data/claude/.credentials.json on host. Survives docker compose down / image rebuild / machine reboot.| Mode | How | Where state lands | Survives restart |
|---|---|---|---|
| Claude Max / Pro OAuth Recommended | CloudCLI UI · paste-code OAuth · same flow as desktop Claude Code. Uses your existing Max subscription, no extra charge. | ./data/claude/.credentials.json + ./data/claude/.claude.json.persist |
Yes · via bind mount |
| Anthropic API key | ANTHROPIC_API_KEY=sk-ant-... in compose environment: · or paste in CloudCLI UI. Pay-per-token billing. |
Compose env var (host) or ./data/claude/ if pasted in UI |
Yes · compose file or bind mount |
| Ollama / self-hosted Anthropic-compatible | ANTHROPIC_AUTH_TOKEN=ollama + ANTHROPIC_BASE_URL=<endpoint> in compose env |
Env vars only · no token file written | Yes · in compose file |
Each agent's auth state lives under ./data/claude/ too, symlinked into the right home-relative path on boot by entrypoint.sh. Codex has its own device-auth flow (codex login --device-auth) and can use ChatGPT Plus/Pro subscriptions; or set OPENAI_API_KEY. Gemini reads GEMINI_API_KEY. Cursor reads CURSOR_API_KEY. Codex callback flow (if you want browser-redirect instead of device code) needs port 1455 exposed in compose.
ainb container subcommand should stay out of the OAuth flow entirely. Render the compose at ~/.agents-in-a-box/holyclaude/compose.yaml with absolute bind-mount paths — ~/.agents-in-a-box/holyclaude/claude/ for auth state, ~/.agents-in-a-box/holyclaude/workspace/ for code — so cwd / worktree semantics never bite. Expose http://localhost:3001, open the user's browser there, and let CloudCLI's paste-code OAuth do its thing. Optionally support --api-key / --env-file flags that inject ANTHROPIC_API_KEY into the rendered .env for headless / CI setups where the browser OAuth path is not available.
Weighted against the user's stated goal: "run agents as containers without being bothered about upstream Docker maintenance". Each criterion scores 1–5; weighted total / 5.0.
ainb container up?Four shapes evaluated. A & B both pull HolyClaude as an image dep — differ only in wiring layer. C owns the image. D ships nothing.
Same end state (HolyClaude container running on Docker, agents inside, CloudCLI UI on :3001). Difference is the layer that owns the docker shell-out.
docker compose up/down/exec/logs. Publishes containers.status on snapshot bus so future plugins / TUI panels can render state.host/process/spawn, the plugin cannot launch docker. Manifest already declares the cap; linker has nothing to wire.What plugins can call today vs. what a container-launcher plugin would need. Rust row = missing.
| Host fn | Capability gate | Status | Needed by container plugin? |
|---|---|---|---|
host/snapshot/get · publish · subscribe | event_bus | Shipped | Yes — for containers.status topic |
host/action/invoke | event_bus | Shipped | Optional — RPC to other plugins |
host/fs/read_dir · read_file | read_claude_logs / read_codex_logs | Shipped | Maybe — for compose template lookup |
host/network/fetch | network (allowlist) | Shipped | Optional — Docker Hub tag check |
host/log | none | Shipped | Yes — tracing |
host/process/spawn | spawn_subprocess | Gap · Phase 7+ | Required — launch docker compose |
host/process/wait · kill · signal | spawn_subprocess | Gap · Phase 7+ | Required — lifecycle |
host/process/stdio_stream | spawn_subprocess | Gap · Phase 7+ | Required — docker logs --follow |
User experience for ainb container up on a fresh machine.
ainb container updocker pullcoderluii/holyclaude:1.2.2. Skipped on cache hit.docker compose up -d~/.agents-in-a-box/holyclaude/ (per-user, shared across worktrees).http://localhost:3001 · CloudCLI OAuth flow runs in browser.containers.statusDarker green = stronger fit · rust = weak. Bottom row = weighted totals.
| A · host-side | B · wasm plugin | C · own image | D · do nothing | |
|---|---|---|---|---|
| User value delivered (0.40) | 5 | 5 | 4 | 1 |
| Time to ship (0.15) | 5 | 1 | 2 | 5 |
| Plugin-model coherence (0.15) | 2 | 5 | 3 | 5 |
| Maintenance burden (0.15) | 4 | 3 | 1 | 5 |
| Supply-chain resilience (0.075) | 3 | 3 | 5 | 5 |
| License clarity (0.075) | 4 | 4 | 5 | 5 |
| Weighted total | 4.18 | 3.88 | 3.25 | 3.40 |
Six criteria as a hexagon. Larger area = stronger overall fit; spikes reveal where each option dominates or fails.
Based on the weighted scores. A wins on user value + time-to-ship; B is the migration target.
HolyClaude is designed as a single long-running container. Stevie works exclusively off volatile worktrees — does that mean a shared per-user container with project bind-mounts swapped, or one container per worktree?
spike before Track A locks bind-mount semanticsGitHub Actions default runners, Fargate, Cloud Run can't grant SYS_ADMIN + seccomp=unconfined + shm_size:2g. Pre-flight should detect and refuse with a clear message rather than fail mid-pull.
The HolyClaude image contains a patched copy of GPL-3.0 siteboon/claudecodeui. Users pulling the image from Docker Hub is not ainb redistribution — but legal review before we publish "this is supported" is prudent.
14 releases in 3 weeks then 5 weeks of silence as of 2026-05-15. Pin 1.2.2 and monitor. If upstream dies, fallback is Track C — fold the relevant Dockerfile bits into ainb under a custom tag.
~/.claude across worktrees?If two ainb worktrees both run ainb container up, do they share OAuth state or get isolated trees? Sharing risks token collisions; isolating doubles login flows.
containers.status?Whichever shape (Track A or B), the wire format for containers.status should land now so future TUI panels and the eventual plugin migration are a swap. Suggest minimal msgpack struct: id, image, state, ports, started_at.