Options Paper · trade-off analysis

HolyClaude as an ainb container runtime

Date2026-05-15 AuthorStevie For decision byainb maintainer · 2026-06-01 Draft · 4 options laid out
Recommendation · lead

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.

01

Problem

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.

02

What HolyClaude actually offers

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.

Shape
Docker image + compose templateno CLI, no SDK, no daemon
Registry
Docker Hubcoderluii/holyclaude:1.2.2 · :slim
Arch
linux/amd64 + linux/arm64native arm, not emulated
License
MIT (own code)vendored CloudCLI GPL-3.0

In the box

Bundled in the image · zero ainb work to enable
7 Agent CLIs · pre-installed
Claude Code Codex (OpenAI) Gemini CLI Cursor TaskMaster AI Junie OpenCode
Web UI · CloudCLI on :3001
  • OAuth flow runs in browser — no env-var token wiring needed
  • Session management UI for all 7 agents
  • Patched fork of siteboon/claudecodeui · GPL-3.0 (vendored tarball)
Browser stack for agents
  • Chromium
  • Playwright
  • Xvfb (virtual display)
  • 50+ dev tools
Operational glue · s6-overlay PID 1
  • UID/GID remap on boot (PUID/PGID) — fixes bind-mount perms
  • First-boot bootstrap seeds settings.json, CLAUDE.md, git identity
  • 60s auth-sync loop persists ~/.claude.json to host bind mount
  • Symlinks ~/.codex, ~/.gemini, ~/.cursor configs from ./data/claude
Notifications · Apprise · 100+ backends
  • Wired for Claude Code stop + error events
  • Codex + Gemini event hooks since v1.1.7
  • Discord · Telegram · Slack · Pushover · Gotify · email · etc.
  • Toggle via flag file ~/.claude/notify-on
Local-model routing · Ollama
  • Set ANTHROPIC_AUTH_TOKEN=ollama + ANTHROPIC_BASE_URL=<endpoint>
  • Routes Claude Code traffic to local Ollama instance
Config surface
  • Host .env: HOLYCLAUDE_HOST_PORT, _CLAUDE_DIR, _WORKSPACE_DIR
  • Container env: TZ, PUID, PGID, ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, CURSOR_API_KEY
  • Claude settings & memory at ./data/claude/{settings.json,CLAUDE.md}
  • Two image variants: full (:latest) · slim (:slim)
Bind mounts & ports
  • ./data/claude → /home/claude/.claude — creds + memory
  • ./workspace → /workspace — user code
  • Optional named vol cloudcli-data → /home/claude/.cloudcli — CloudCLI SQLite
  • Port :3001 exposed · dev ports 3000,5173,8787,9229,1455 commented out

Not in the box

Gaps ainb still owns · or has to work around
No programmatic surface
  • No holyclaude CLI binary
  • No SDK or library API
  • No daemon process (the container is the daemon)
  • Integration = raw docker + docker compose
No workflow features
  • No branch-per-task / session snapshotting
  • No cost tracking
  • No multi-agent orchestration
  • No docker exec / attach wrapper commands
No granularity controls
  • One container per user/server — not per-session, not per-project
  • Per-worktree containers require ainb to template the compose file
  • Auth tree shared across all sessions inside the single container
Runtime requirements ainb must pre-flight
  • Linux caps: SYS_ADMIN + SYS_PTRACE + seccomp=unconfined
  • shm_size: 2g required (Chromium)
  • Breaks on restricted CI (GH Actions default, Fargate, Cloud Run)
  • SQLite over SMB/NFS broken — CloudCLI account vol must be local
Upstream risks
  • 5-week silent commit gap (Mar 22 → Apr 10 2026, then quiet)
  • CloudCLI patches are minified-JS sed/perl rewrites — break on every upstream bump
  • No automated tests in the repo
  • "Continue in Shell" CloudCLI button permanently broken
03

Auth workflow · Claude Max via OAuth

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

Your host (mac / linux)
~/.claude/
Your personal Claude Code account if you use Claude CLI on the host.
Untouched. HolyClaude never reads or writes here.
./data/claude/
Host directory you pick in compose (override via HOLYCLAUDE_HOST_CLAUDE_DIR). This is the bind-mount source. Where the container's auth state actually lives, on your disk.
bind mount same bytes
HolyClaude container
/home/claude/.claude/
Mounted from ./data/claude/ — same bytes. Where the claude CLI inside the container looks for credentials, settings, memory.
/home/claude/.claude.json
Claude Code session file (OAuth state). Copy-persisted on boot, synced every 60s to ./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.

First-run OAuth · Max / Pro plan

step 01
Boot container
docker compose up -d · sentinel-gated bootstrap.sh runs once.
step 02
Open CloudCLI UI
Host browser → http://localhost:3001. Create a CloudCLI account (10s, local SQLite, container-local).
step 03
"Sign in to Claude"
CloudCLI runs claude CLI inside the container. CLI prints OAuth URL + waits for paste-back code.
step 04
claude.ai login
Open the URL in a browser tab · pick your Max workspace · auth completes · claude.ai shows a code on screen.
step 05
Paste code
Back in CloudCLI UI · paste into the terminal panel. No callback server needed inside the container — that's the whole point of paste-code OAuth.
step 06
Token written
claude CLI exchanges code → writes /home/claude/.claude/.credentials.json.
step 07
Bind-mount persists
Same file now visible at ./data/claude/.credentials.json on host. Survives docker compose down / image rebuild / machine reboot.

Three auth modes · pick one

ModeHowWhere state landsSurvives 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

Other agents (Codex, Gemini, Cursor) follow the same pattern

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.

For ainb integration (Track A): the 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.
04

Evaluation criteria

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.

User value delivereddoes this fulfill the stated goal?
0.40
Time to shipwhen can users ainb container up?
0.15
Plugin-model coherencefits ainb's "everything is a plugin" north star
0.15
Maintenance burden on ainb teamdirect user constraint — "don't bother me with Docker"
0.15
Supply-chain resilienceif HolyClaude goes dormant, what?
0.075
License clarityGPL-3.0 vendored CloudCLI boundary
0.075
05

Options

Four shapes evaluated. A & B both pull HolyClaude as an image dep — differ only in wiring layer. C owns the image. D ships nothing.

Track APick

Host-side ainb container

Subcommand drives docker compose directly against pinned HolyClaude image. Bundled template, pre-flight checks, status snapshot.

User val5
Time ship5
Coherent2
Maint4
Supply3
License4
weighted4.18 / 5
Ships now. Plugin-model debt is real but bounded — every host-side concern is callable from a future plugin once Phase 7 lands.
Track B

ainb-plugin-holyclaude

wasm plugin owns the full data plane. Pure plugin pattern — host stays Docker-unaware. Blocked on host/process/spawn host fn (Phase 7+).

User val5
Time ship1
Coherent5
Maint3
Supply3
License4
weighted3.88 / 5
Architecturally correct. Migration target once subprocess host fns ship. Fuel-budget concerns for docker exec log streaming need design.
Track C

Build & ship our own Dockerfile

Roll our own thin image. No HolyClaude dependency. Insurance against upstream dormancy. Pure-MIT bundle.

User val4
Time ship2
Coherent3
Maint1
Supply5
License5
weighted3.25 / 5
Loses on the user's stated constraint. Maintaining a Dockerfile that tracks 7 agent CLIs is exactly what the user said they don't want to do.
Track D

Do nothing · doc-only

Ship a docs page showing manual docker run coderluii/holyclaude. No ainb integration. Wait for Phase 7+.

User val1
Time ship5
Coherent5
Maint5
Supply5
License5
weighted3.40 / 5
Honest null hypothesis. Scores well on everything except the thing that matters — user value. Useful only as a comparison anchor.
06

Architecture · A vs B

Same end state (HolyClaude container running on Docker, agents inside, CloudCLI UI on :3001). Difference is the layer that owns the docker shell-out.

Track A · host-side

ainb host owns the docker shell-out

AINB HOST (rust) ainb container up docker DOCKER ENGINE compose · pull · exec coderluii/holyclaude:1.2.2 HolyClaude container Claude Code Codex Gemini CLI Cursor TaskMaster Junie OpenCode CloudCLI web UI · localhost:3001
Ships now. Host owns docker compose up/down/exec/logs. Publishes containers.status on snapshot bus so future plugins / TUI panels can render state.
Track B · wasm plugin (Phase 7+)

Plugin owns the docker shell-out

AINB HOST CLI router json-rpc ainb-plugin-holyclaude wasm32-wasip1 cap: spawn_subprocess host fn (gap) host/process/ spawn phase 7+ DOCKER ENGINE compose · pull · exec coderluii/holyclaude:1.2.2 HolyClaude container · 7 agents Claude Codex Gemini Cursor TaskMstr Junie OpenCode CloudCLI · localhost:3001
Blocked. The dashed rust box is the missing piece. Without host/process/spawn, the plugin cannot launch docker. Manifest already declares the cap; linker has nothing to wire.
07

The blocker · host-fn catalogue

What plugins can call today vs. what a container-launcher plugin would need. Rust row = missing.

Host fnCapability gateStatusNeeded by container plugin?
host/snapshot/get · publish · subscribeevent_busShippedYes — for containers.status topic
host/action/invokeevent_busShippedOptional — RPC to other plugins
host/fs/read_dir · read_fileread_claude_logs / read_codex_logsShippedMaybe — for compose template lookup
host/network/fetchnetwork (allowlist)ShippedOptional — Docker Hub tag check
host/lognoneShippedYes — tracing
host/process/spawnspawn_subprocessGap · Phase 7+Required — launch docker compose
host/process/wait · kill · signalspawn_subprocessGap · Phase 7+Required — lifecycle
host/process/stdio_streamspawn_subprocessGap · Phase 7+Requireddocker logs --follow
08

Track A · first-run UX

User experience for ainb container up on a fresh machine.

step 01
User runs ainb container up
Single CLI invocation. No flags needed for default.
step 02
Pre-flight
Docker engine present? Caps available? Port 3001 free? Arch supported?
step 03
docker pull
Pinned coderluii/holyclaude:1.2.2. Skipped on cache hit.
step 04
docker compose up -d
Bundled compose template. Absolute bind mounts under ~/.agents-in-a-box/holyclaude/ (per-user, shared across worktrees).
step 05
Open browser
http://localhost:3001 · CloudCLI OAuth flow runs in browser.
step 06
Publish containers.status
Event-bus snapshot for future TUI panel / plugin to render state.
09

Comparison matrix

Darker 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)5541
Time to ship (0.15)5125
Plugin-model coherence (0.15)2535
Maintenance burden (0.15)4315
Supply-chain resilience (0.075)3355
License clarity (0.075)4455
Weighted total4.183.883.253.40
10

Trade-off radar

Six criteria as a hexagon. Larger area = stronger overall fit; spikes reveal where each option dominates or fails.

user value time ship coherent maint supply license
  • A · host-side (leader)
  • B · wasm plugin
  • C · own image
  • D · do nothing
11

Tentative ranking

Based on the weighted scores. A wins on user value + time-to-ship; B is the migration target.

1stA · host-side4.18
2ndB · wasm plugin3.88
3rdD · do nothing3.40
4thC · own image3.25
12

Open questions

Per-user, per-worktree, or per-project container?

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 semantics

CI environments without SYS_ADMIN — graceful refuse or fallback?

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

pre-flight design

GPL-3.0 vendored CloudCLI — redistribution boundary?

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.

legal check

HolyClaude project health — insurance plan?

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.

monitor commit cadence

Auth multiplexing — shared ~/.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.

UX call

Snapshot bus design for 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.

topic design