diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fa41d4d..d53486d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,7 +29,12 @@ jobs:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Stamp CLI version
- run: sed -i "s/__CLI_VERSION__/$(node -p 'require(\"./package.json\").version')/" bin/aibox
+ # Anchor to the assignment line ONLY: the script contains two more
+ # __CLI_VERSION__ occurrences that are runtime dev-build sentinels
+ # ([[ "$X" == "__CLI_VERSION__" ]]) — replacing those too would make
+ # every released build identify as "dev" (wrong image tag, dead
+ # update mechanism).
+ run: sed -i "s/^CLI_VERSION=\"__CLI_VERSION__\"/CLI_VERSION=\"$(node -p 'require(\"./package.json\").version')\"/" bin/aibox
- run: npm publish --access public --provenance
homebrew:
diff --git a/.gitignore b/.gitignore
index dfa36fa..eaf3961 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,3 @@
node_modules/
.DS_Store
-compose.dev.yaml
-.aibox
.idea/
-.idea/workspace.xml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7ce2911..8df0eee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,6 +16,8 @@ To test locally, symlink into your PATH:
ln -sf "$(pwd)/bin/aibox" /usr/local/bin/aibox
```
+Dev-mode note: a checkout runs as version `dev`, so the image tag doesn't change between your edits — after modifying the embedded Dockerfile or entrypoint, force a rebuild with `docker rmi aibox:dev-node` (releases bump the tag, so users get rebuilds automatically).
+
## Publishing
Publishing is fully automated. Pushing a version tag triggers CI which creates a GitHub release, publishes to npm, and updates the Homebrew tap.
@@ -36,4 +38,8 @@ npm run release
1. Update `version` in `package.json`
2. Commit, then `npm run release` — CI handles the rest
-Note: `AIBOX_VERSION` in `bin/aibox` is separate — it tracks the Docker image format and only needs bumping when the Dockerfile or entrypoint changes (triggers automatic image rebuild for users).
+Note: the Docker image tag is derived from the CLI version (`aibox:-node`), so any release automatically rebuilds users' images and recreates their containers on next run — sessions and login live in the `aibox-home` volume and are unaffected. In a git checkout (unstamped `__CLI_VERSION__`) the tag is `aibox:dev-node`.
+
+Two stamping caveats:
+- The release workflow's sed must replace ONLY the `CLI_VERSION=` assignment — the script contains two more `__CLI_VERSION__` occurrences that are runtime dev-build sentinels and must survive stamping.
+- The Homebrew formula installs from the raw git tag, which is unstamped. The tap formula should `inreplace` the same assignment during install; until it does, brew installs behave like dev builds (image `aibox:dev-*`, no update notice).
diff --git a/README.md b/README.md
index 1d5627e..ee36ec7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
aibox
-
Instant Docker sandboxes for AI coding agents
+
Persistent Docker sandboxes for Claude Code
@@ -8,224 +8,122 @@
-
-
-> *Skip permission prompts safely. Let agents run wild. Tear everything down when you're done.*
+> *One command into a sandboxed Claude Code. Nothing gets destroyed behind your back.*
```bash
-cd myproject && aibox claude --yolo
+cd myproject && aibox
```
-One command to go from bare project to fully isolated Claude Code session. Changes sync both ways, the agent stays sandboxed, tear everything down when you're done.
+aibox runs Claude Code inside a Docker container, so the agent can run wild while your Mac stays clean. Add `--yolo` to skip all permission prompts. One container per project, all sharing a single persistent home volume — one login, per-project session history, everything survives.
## Quickstart
```bash
npm install -g aibox-cli # 1. install
-cd myproject # 2. go to your project
-aibox claude --yolo # 3. run
-```
-
-## Features
-
-- **Zero config** — don't even need Docker installed. Detects your machine, auto-installs Colima/Docker, builds an Alpine image with Claude Code + dev tools on first run
-- **Safe by default** — network firewall (allowlisted domains only), restricted sudo, sensitive file detection. `--yolo` to unlock everything
-- **Full isolation** — `--copy` snapshots into a Docker volume, `--worktree` creates a git worktree. Both handle uncommitted changes, submodules, and LFS
-- **Parallel agents** — run multiple named instances on the same project, each with its own container
-- **Editor integration** — VS Code, Cursor, JetBrains, Windsurf — set startup command to `aibox claude --yolo`
-- **Clone and run** — `--repo ` clones any git repo and launches an agent session
-- **Not just Claude** — container ships with Node.js, python3, git, ripgrep, build tools. Run aider, codex, or anything else
-- **Just a shell script** — no daemon, no runtime dependencies, easy to fork
-
-## Install
-
-```bash
-npm install -g aibox-cli
-# or
-brew install blitzdotdev/tap/aibox
+cd myproject # 2. go to your project
+aibox # 3. run (builds the image on first use)
```
-
-Prerequisites
+## How it works
-On macOS, if Docker isn't installed, aibox will offer to install [Colima](https://github.com/abiosoft/colima) + Docker via Homebrew automatically. Also works with [Docker Desktop](https://www.docker.com/products/docker-desktop/) or [OrbStack](https://orbstack.dev).
+- **One container per project directory.** `aibox` in a project creates (or re-attaches to) that project's container. Open more terminal tabs and run `aibox` again — they attach to the same container.
+- **Your project is bind-mounted at its real path.** Changes sync both ways, paths inside the container match your Mac.
+- **One home volume, sliced per project.** Claude login, settings, and the `claude` binary live in shared slices of the `aibox-home` volume — log in once, forever. Everything else in a project's home (ssh keys, shell history, caches, and that project's sessions) is a private slice only its own containers mount, so one project's agent can't read another project's home directory or session transcripts. (Claude's shared bookkeeping — settings, todos, file-history — stays common; the private slices are the home dir and transcripts.) Volumes from older aibox versions migrate to this layout automatically on first run, with a safety backup taken first.
+- **Nothing is destroyed implicitly.** Exiting Claude leaves the container running in the background (idle containers cost ~nothing) — the next `aibox` attaches instantly. `aibox stop` stops it; a stopped container keeps everything, including packages you apt-installed. Containers are only recreated when the image changes, and the home volume survives even that.
+- **The container is the sandbox.** Full sudo inside. Permission prompts are on by default, but bypass mode is always available in-session (aibox passes claude's `--allow-dangerously-skip-permissions`); run `aibox --yolo` to start with all prompts skipped (`--dangerously-skip-permissions`).
+- **Disposable copies on demand.** `aibox --copy` runs Claude in a fresh container on a *snapshot* of the project instead — nothing is bind-mounted, so the agent physically can't touch your real files. Same login and session history (shared home volume), own dev URLs (`.-copy.aibox.localhost`). The container is removed when the session exits; keep work by committing and pushing from inside. Each `--copy` run is its own independent sandbox. Combines with `--yolo`, and works for any program via `aibox run --copy`.
-
+## Dev servers
-## Usage
+Anything listening on any port inside the container is instantly reachable from your browser:
-```bash
-aibox up # start container (auto-builds image on first run)
-aibox claude --yolo # no prompts, full sudo, no firewall
-aibox claude --safe # keep prompts, restricted sudo, firewall on
-aibox claude # asks you each time
-aibox claude --resume # resume most recent conversation
-aibox shell # zsh inside the container
-aibox down # stop and remove
```
-
-### Named instances
-
-Run multiple containers for the same project:
-
-```bash
-aibox --name refactor claude --yolo
-aibox --name tests claude --safe
-aibox --name refactor down
+http://..aibox.localhost
```
-### Isolation modes
-
-| Mode | Flag | How it works |
-|------|------|-------------|
-| **Bind mount** | *(default)* | Live-sync project directory |
-| **Copy** | `--copy` | Snapshot into Docker volume (git or non-git) |
-| **Worktree** | `--worktree` | Lightweight git worktree on host |
-
-Both `--copy` and `--worktree` auto-detect uncommitted changes, submodules, and Git LFS. Each creates a `aibox/` branch.
-
-
-Copy mode details
-
-- **Git repo** — uses `git bundle` to clone tracked files (preserves history, excludes .gitignored files). Asks to include uncommitted changes.
-- **Git subfolder** — asks whether to copy the full repo or just the current folder.
-- **Non-git directory** — tars the folder (excluding `node_modules` and `.git`).
-
-
-
-
-Worktree mode details
-
-Creates a `git worktree` at `~/.config/aibox/worktrees/`. Near-instant, shares remotes with the main repo. Requires a git repository. Asks to include uncommitted changes.
+Claude starts `vite` on 5173 in project `myapp` → open `http://5173.myapp.aibox.localhost`. No ports to publish, no restarts, no config — a tiny shared Caddy proxy on the Docker network reaches any container port directly, WebSockets/HMR included.
-
+Works out of the box in Chrome, Edge, and Firefox (`*.localhost` resolves to loopback natively). Safari needs macOS 26+. CLI tools like `curl` need `--resolve` (the system resolver doesn't do `*.localhost`).
-### Clone from URL
+## Phone & browser sessions
```bash
-aibox --repo https://github.com/user/project.git claude --yolo
-aibox --repo git@github.com:user/project.git --branch dev claude
+aibox serve
```
-Repos cached at `~/.config/aibox/repos/` with submodules included.
+runs a small sessions UI at `http://45789..aibox.localhost`. **New session** starts a fresh session you drive from claude.ai/code or the Claude phone app ([Remote Control](https://code.claude.com/docs/en/remote-control)); **Resume** brings any past session back the same way; live sessions show as such and can be stopped. Every session is one detached claude process inside the project's container — closing your terminal changes nothing, and registration is outbound-only HTTPS (no ports, nothing exposed). Any Claude session can pull the same tricks on request — the shared CLAUDE.md teaches it the commands, so you can also say "start me a new session" from your phone in any live chat. `aibox serve stop` ends the UI and every live session of this project. On a remote Linux box, reach the UI with `ssh -L 8080:127.0.0.1:80 host` and open the same URL with `:8080`; the phone side needs no tunnel at all.
-### Port forwarding
+## Backup & restore
-Forward ports from a running container to the host — no restart needed:
+Everything worth keeping is in one volume, so backup is one file:
```bash
-aibox port-forward 3000 # host:3000 → container:3000
-aibox port-forward 8080:3000 # host:8080 → container:3000
-aibox port-forward 3000 5173 # multiple ports
-aibox port-forward --list # show active forwards
-aibox port-forward --stop 3000 # stop one
-aibox port-forward --stop-all # stop all
+aibox backup # ~/aibox-backups/aibox-home--.tar.gz
+aibox backup /some/dir # custom destination
+aibox restore # replaces the volume (auto safety-backup first)
```
-Uses a lightweight sidecar container (`alpine/socat`) on the same Docker network. Cleaned up automatically on `aibox down`.
+Backups are safe to take while sessions are running.
-### Management
+## Migrating from aibox v1
-```bash
-aibox status # list all aibox containers
-aibox volumes # list copy volumes and worktrees
-aibox disk # show disk usage breakdown
-aibox clean # clean everything (containers, volumes, images, sessions)
-aibox clean --volumes # only orphaned volumes
-aibox clean --containers # only stopped containers
-aibox clean --sessions 7 # only session data older than 7 days (default: 30)
-aibox clean --docker # only dangling images + build cache
-aibox clean --force # skip confirmation
-aibox doctor # diagnose common issues
-aibox down --clean # also remove copy volumes / worktrees
-aibox down --all # stop all containers for this project
-aibox nuke # remove ALL aibox containers
-```
-
-Containers auto-stop when the last `claude` or `shell` session exits.
-
-## Security modes
-
-| | `--yolo` | `--safe` (default) |
-|---|---|---|
-| **Permission prompts** | Skipped | Kept |
-| **Sudo** | Full | Restricted (chown only) |
-| **Network** | Unrestricted | Firewall (allowlist only) |
-
-In safe mode, outbound traffic is restricted to Claude API, npm, GitHub, PyPI, DNS, and SSH. Add extra domains:
+v2 is a clean break: one container per project, one shared home volume, and no destructive lifecycle. A standalone script merges all your v1 data — every per-image `aibox-auth-*` volume **and** any old backup folders — into the new volume:
```bash
-export AIBOX_EXTRA_DOMAINS="example.com,api.myservice.io"
-```
-
-## IDE integration
-
-
-JetBrains (WebStorm, IntelliJ, etc.)
+# npm installs ship the script next to the CLI:
+bash "$(npm root -g)/aibox-cli/scripts/migrate-to-v2.sh" [old-backup-dir ...]
-1. Install the [Claude Code plugin](https://plugins.jetbrains.com/plugin/claude-code)
-2. Run `aibox init` in your project
-3. Set the plugin's startup command to `aibox claude --yolo`
+# or fetch it directly:
+curl -fsSL https://raw.githubusercontent.com/blitzdotdev/aibox/main/scripts/migrate-to-v2.sh | bash -s -- [old-backup-dir ...]
+```
-Node.js interpreter is also configured to use the container.
+Sessions merge file-by-file (nothing is ever overwritten or deleted; sources are read-only), `.claude.json` is merged newest-wins, and the script prints — but never runs — the cleanup commands for old v1 resources.
-
+**Order matters:** run this merge **before** your first v2 `aibox` run — that first run slices the volume into the per-project layout, and data merged into the flat layout afterwards would sit unmounted (visible via `aibox sessions`, but not live).
-
-VS Code
+## Commands
-1. Install the [Claude Code extension](https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code)
-2. Set the Claude Code startup command to `aibox claude --yolo`
-3. Or use Dev Containers with the generated `compose.dev.yaml`
+| Command | What it does |
+|---------|-------------|
+| `aibox` / `aibox claude [args]` | Shorthand for `aibox run claude`. `--yolo` skips all permission prompts; `--copy` uses a disposable snapshot container (no bind mount, removed on exit); other args pass through verbatim (`--resume`, `-p`, ...). `aibox --resume` works too |
+| `aibox run [--copy] [args]` | Run any program in the sandbox (e.g. `aibox run codex`). `--copy` works the same as above; the program's own flags pass through |
+| `aibox serve` | Sessions UI in the container: start new phone/claude.ai-drivable sessions, resume past ones, stop live ones. `aibox serve stop` ends the UI and every live session of this project |
+| `aibox sessions` | All projects' sessions on one local page (host-side, loopback-only, foreground). Buttons per session: open in Ghostty/Terminal, copy the resume command, or send to your phone |
+| `aibox shell [cmd]` | zsh in the container, or run a one-off command |
+| `aibox stop [--all]` | Stop this project's container (`--all`: everything incl. proxy). Loses nothing |
+| `aibox status` | Containers with live memory + disk use, dev URLs, Docker disk totals, home volume size |
+| `aibox backup [dir]` | Snapshot the home volume to a tar.gz |
+| `aibox restore ` | Restore a backup (safety-backup of current state first) |
+| `aibox update` | Update the CLI; image rebuilds automatically on next run |
+| `aibox version` / `help` | Versions + docker state / this table's long form |
-
+## Config
-
-Cursor / Windsurf / Other editors
+`~/.aibox/config` (key=value, all optional):
-Set your agent's startup command to `aibox claude --yolo`. Works anywhere you can configure a shell command.
+```
+node_version=24 # base image: node:-bookworm
+proxy_port=80 # host port for the dev-server proxy
+backup_dir=~/aibox-backups
+```
-
+The image is `node:-bookworm` (Debian) plus a few basics (zsh, sudo, ripgrep, fzf, jq, less, procps, curl) — Claude apt-installs anything else on demand, and it persists across stop/start. To make custom tooling survive image rebuilds too, put extra Dockerfile lines in `~/.aibox/Dockerfile.extra`.
-## Other agents
+## Prerequisites
-The container ships with Node.js 20, git, git-lfs, ripgrep, zsh, python3, and build tools. Claude Code is pre-installed, but you can run anything:
+Built for macOS; works on Linux too. Needs Docker Engine 26+ (any 2024-or-later runtime; aibox checks and tells you if not). On macOS, Docker via [Colima](https://github.com/abiosoft/colima), [OrbStack](https://orbstack.dev), or [Docker Desktop](https://www.docker.com/products/docker-desktop/):
```bash
-aibox shell # then run: aider, codex, etc.
+brew install colima docker && colima start
```
-Customize the Dockerfile at `~/.config/aibox/Dockerfile`.
-
-## CLI reference
-
-| Short | Long | Description |
-|-------|------|-------------|
-| `-n` | `--name NAME` | Named instance (multiple containers per project) |
-| `-d` | `--dir PATH` | Run in a different project directory |
-| `-r` | `--repo URL` | Clone a git repo and run in it |
-| `-b` | `--branch NAME` | Branch to checkout (with `--repo`) |
-| `-i` | `--image NAME` | Override base Docker image |
-| `-c` | `--copy` | Copy project into Docker volume (full isolation) |
-| `-w` | `--worktree` | Use git worktree (lightweight isolation) |
-| `-y` | `--yolo` | Skip prompts, full sudo, no firewall |
-| `-s` | `--safe` | Keep prompts, restricted sudo, firewall on |
-| | `--all` | With `down`: stop all project containers |
-| | `--clean` | With `down`: also remove copy volumes / worktrees |
-| | `--force` | With `clean`: skip confirmation prompts |
+aibox auto-starts an installed-but-stopped runtime; it won't install one for you.
-## Config
-
-Per-project settings in `.aibox`:
-
-```
-IMAGE=aibox:latest
-SHARED_MODULES=true
-```
+Note on the dev-server proxy port: the proxy asks Docker for `127.0.0.1:80` only, but some Colima versions ignore the loopback restriction and publish the port on your LAN. Everything behind it is your own sandboxed dev traffic, but if that matters to you, keep Colima current (or use OrbStack/Docker Desktop).
## Contributing
-See [CONTRIBUTING.md](CONTRIBUTING.md).
+See [CONTRIBUTING.md](CONTRIBUTING.md). Design/requirements for v2 are in [REVAMP.md](REVAMP.md).
## License
diff --git a/REVAMP.md b/REVAMP.md
new file mode 100644
index 0000000..5faaf7f
--- /dev/null
+++ b/REVAMP.md
@@ -0,0 +1,472 @@
+# aibox v2 — Revamp Requirements
+
+Requirements for rewriting aibox around how it is actually used: one trusted
+machine, one user, Claude Code in a sandbox, containers that are cheap to
+enter and impossible to lose data in.
+
+This document is the spec for the rewrite. It records the decisions already
+made (with rationale), the exact command surface, the architecture, and what
+gets deleted. A separate one-time migration script (also specced here) moves
+existing data into the new layout.
+
+---
+
+## 1. Why
+
+The current `bin/aibox` is 2,379 lines, 16 commands, and optimizes for
+safety features and isolation modes that go unused, while getting the one
+thing that matters wrong: **data durability**.
+
+Concrete problems in the current implementation:
+
+- **Destructive lifecycle by default.** When the last `claude`/`shell`
+ session exits, the script silently runs `docker compose down`
+ (`bin/aibox:1154-1170`). The container is *removed*, not stopped — every
+ apt package, global npm install, and any file outside a mounted volume is
+ destroyed. This has caused real data loss. A second destructive path:
+ running `aibox claude` in a different mode than the container was started
+ with (`--yolo` vs `--safe`) also `down`s the running container
+ (`bin/aibox:1447-1453`).
+- **Fragmented session history.** Auth/session state lives in a volume named
+ per image — `aibox-auth-` (`bin/aibox:1068`). Change the image tag
+ and you get a fresh volume: new login, empty session list, old sessions
+ stranded in the previous volume. Multiple such volumes now exist, each
+ holding a slice of history.
+- **No first-class backup.** Backups currently depend on an external
+ hand-rolled script.
+- **Port forwarding is manual and clunky.** Each forwarded port is a
+ `alpine/socat` sidecar container created by hand (`aibox port-forward`).
+ In practice this was abandoned in favor of asking Claude to open a
+ Cloudflare quick tunnel — workable, but slow, public, and not ideal.
+- **Unused surface area.** Safe mode + domain-allowlist firewall, restricted
+ sudo, `--copy` / `--worktree` isolation, named instances, `--repo` clone
+ mode, compose orchestration, `init` / WebStorm integration, `clean`,
+ `nuke`, `doctor`, `volumes`, `disk` — none of it is used. It exists to
+ serve hypothetical users, and it is where the 2,400 lines went.
+- **It writes into your project.** Plain `aibox up` auto-runs init when
+ `compose.dev.yaml` is missing (`bin/aibox:1314-1327`), dropping
+ `compose.dev.yaml`, `.aibox`, and `.idea/workspace.xml` into the project
+ and editing its `.gitignore`.
+- **Greedy global flag parsing.** All argv is scanned for aibox flags before
+ dispatch (`bin/aibox:251-325`), so `aibox claude -c` becomes aibox's
+ `--copy` instead of Claude's `--continue`; interactive prompts plus
+ unconditional `docker exec -it` also make non-interactive use
+ (`claude -p` in a pipe) impossible.
+
+## 2. Product decisions (settled)
+
+These were decided explicitly; the rewrite must not relitigate them.
+
+| # | Decision | Choice | Rationale |
+|---|----------|--------|-----------|
+| D1 | Language / distribution | Single bash file, published to npm as `aibox-cli` (same as today) | New scope is ~500 lines, not 2,400; zero runtime deps beyond Docker |
+| D2 | Security posture | Permission prompts on by default with bypass selectable in-session (`--allow-dangerously-skip-permissions`); `aibox --yolo` starts in bypass (`--dangerously-skip-permissions`). No firewall, no restricted sudo | Isolation comes from the container boundary itself; the prompt default is the only mode switch |
+| D3 | Container lifecycle | Container **keeps running in the background** when the last session exits. `aibox stop` stops it explicitly. It is only ever *removed* when the image changes (and then only recreated, never left absent) | Instant re-attach; idle containers cost ~nothing; kills the data-loss footgun |
+| D4 | Instances | Exactly one persistent container per project directory; multiple terminals just `exec` into the same container. No named instances. `--copy` additionally spawns a disposable container per run (unique name, removed on exit) | Matches real usage; disposable runs must not fight over one name |
+| D5 | Session/home storage | One **global** named volume `aibox-home`, mounted in **slices** (`volume-subpath`, Engine 26+): shared slices for the Claude login/settings and binary, a private per-project slice for each project's home and transcripts. Old flat volumes migrate in place automatically (safety backup first; unrecognized files never moved) | One login and one backup tar stay; project-vs-project isolation is enforced at the mount level — the only level that means anything with full sudo inside |
+| D6 | Project mount | Bind-mount the project directory at **the same absolute path as on the host** (today's default-mode behavior, `bin/aibox:475`). `--copy` skips the bind mount and streams a tar snapshot to the same path instead | Claude session keys are derived from cwd — keeping the path keeps every existing session valid with zero migration (copy mode included, since the path matches) |
+| D7 | Backup | Built-in `aibox backup` / `aibox restore` — clean and simple, must actually work | Replaces the external script |
+| D8 | Migration of old data | A **separate standalone script** (not part of the CLI) that merges both live `aibox-auth-*` volumes **and** old backup folders into the new `aibox-home` volume | One-time operation; keeps the CLI clean |
+| D9 | Dev-server access | Host-side reverse proxy with wildcard subdomains: `http://..aibox.localhost` → container port. Replaces port-forward sidecars and ad-hoc Cloudflare tunnels for local use | `*.localhost` (multi-level included) resolves to loopback natively in Chrome/Edge and Firefox 84+ with zero setup, no sudo, no dnsmasq; Safari only gained this on macOS 26 Tahoe (WebKit bug 160504). CLI tools using the system resolver (curl) don't resolve it — documented workaround, not solved. `/etc/hosts` can't do wildcards, so there is no hosts-file step |
+| D10 | Base image | `node:${node_version}-bookworm` (Debian 12), `node_version` configurable in `~/.aibox/config`, default = current Active LTS (`24` as of mid-2026; Node 22 entered maintenance Oct 2025, Node 20 is EOL) | Debian-based official node images are the standard container base; glibc means nothing is uninstallable. Node version is a one-line config change |
+| D11 | Command surface | Exactly the commands in §3 (including the post-spec `sessions`/`rc-resume` additions). Everything from v1 outside it is deleted | See §8 for the deletion list |
+
+## 3. Command surface
+
+```
+aibox # same as `aibox claude`
+aibox claude [args...] # shorthand for `aibox run claude`; --yolo skips all prompts, --copy uses a disposable snapshot container, other args pass through (--resume, -c, etc.)
+aibox run [--copy] [args...] # ensure image/container/proxy, then run any program inside (e.g. aibox run codex)
+aibox serve # sessions UI (port 45789) in the container: new/resume/stop phone-drivable sessions, each a detached claude --remote-control process; serve stop ends it all
+ # (known trade-off: the UI binds container-wide for the proxy, so other containers on the aibox network can reach it; its endpoints only spawn/stop THIS project's sessions)
+aibox sessions # host-side all-projects page (loopback :45790, foreground): expand/search every project's sessions, open in a terminal, copy the resume command, or send to the phone
+aibox rc-resume # plumbing for the above: make one past session of this project phone-drivable
+aibox shell [cmd...] # zsh in the container, or run a one-off command
+aibox stop [--all] # stop this project's container (--all: every aibox container + proxy). Never deletes anything
+aibox status # all aibox containers: project, state, uptime, image; proxy URLs; volume size
+aibox backup [dir] # snapshot aibox-home volume to a tar.gz on the host
+aibox restore # restore a backup into aibox-home (with automatic pre-restore safety backup)
+aibox update # update CLI (npm), rebuild image if needed, mark containers for recreation
+aibox version # CLI version, image version, docker info
+aibox help
+```
+
+Rules:
+
+- Unknown command → one-line error with a help hint, exit 1. No interactive prompts anywhere except
+ destructive confirmations (`restore`) and first-run niceties.
+- Everything after `claude` passes through to claude, except aibox's own
+ `--copy`/`--yolo`, which are consumed wherever they appear; a `--` ends
+ aibox flag parsing. Nothing else gets swallowed the way v1's parser ate
+ `-c` (`bin/aibox:251-325`).
+- Allocate a TTY (`docker exec -it`) only when stdin is a terminal, so
+ `aibox claude -p "..."` and piped/scripted use work; v1 hard-coded `-it`.
+- `run`/`claude`/`shell`/`serve`/`rc-resume` create everything they need;
+ everything they need (image, network, volume, container, proxy) is created
+ idempotently on demand. There is no `up`/`build`/`init`.
+- Keep v1's non-blocking update notice (`bin/aibox:2304-2326`): a cached,
+ 24h-throttled npm version check that prints a one-line hint when a newer
+ `aibox-cli` exists. It must never block or slow a command.
+- Every command must be safe to run concurrently from multiple terminals
+ (two tabs running `aibox claude` simultaneously on the same project must
+ not race the container creation — use a lock or tolerate-exists creation).
+
+## 4. Architecture
+
+### 4.1 Naming and identity
+
+- Project identity = absolute path of the project directory (the cwd where
+ `aibox` is invoked; symlinks resolved).
+- Container name: `aibox--` where `` is the sanitized
+ directory basename and `` is the first 6 hex chars of the SHA-256 of
+ the absolute path. Two directories both named `app` never collide.
+- Proxy hostname label: `` (see §5); on slug collision between two
+ *running* projects, the later one uses `-`. `aibox status`
+ always shows the canonical URL.
+- Docker network: `aibox` (bridge), shared by all project containers and the
+ proxy.
+- Volume: `aibox-home`, mounted at `/home/aibox` in every project container.
+- Image: `aibox:` (e.g. `aibox:2.0.0`), so image staleness is
+ detectable by tag comparison alone.
+
+### 4.2 Container spec
+
+- Plain `docker run` / `docker exec` — **no docker compose**, no generated
+ YAML. One container needs no orchestrator, and compose's `down` semantics
+ were part of the original footgun.
+- Mounts (see D5 — five volume-subpath slices of `aibox-home`):
+ - `projects//home` → `/home/aibox` (private)
+ - `shared/claude-cfg` → `/home/aibox/.claude` (login/settings, shared)
+ - `projects//claude-projects` → `/home/aibox/.claude/projects` (private transcripts)
+ - `shared/local-bin` + `shared/claude-app` → the claude binary (shared)
+ - `:` (bind, rw), workdir = that path
+- `CLAUDE_CONFIG_DIR=/home/aibox/.claude` (unchanged from v1, so existing
+ `.claude` contents drop in as-is).
+- User `aibox` (uid 1000), passwordless sudo, login shell zsh. Note:
+ `node:*-bookworm` ships a `node` user already occupying uid 1000 — the
+ Dockerfile must remove it (`userdel -r node`) before creating `aibox`.
+- `--add-host host.docker.internal:host-gateway` so the container can reach
+ services on the Mac (kept from v1).
+- `docker exec` forwards `TERM`, `COLORTERM`, `LANG`, and any `ANTHROPIC_*`
+ vars set on the host (kept from v1; the IDE-integration env plumbing is
+ not kept).
+- No host dotfiles are mounted (`.gitconfig`, `~/.ssh` — same as v1). This
+ is fine in v2 precisely because the home volume persists: configure git
+ identity or SSH keys once inside the container and they stick forever.
+- Long-running init process: `sleep infinity` (or equivalent) so the
+ container stays up independent of sessions. `--restart unless-stopped` so
+ it survives Docker/Colima restarts. No healthcheck needed.
+- Sessions are `docker exec -it` — a session ending never affects the
+ container.
+
+### 4.3 Lifecycle
+
+```
+aibox claude/shell:
+ docker running? → if colima installed but stopped: colima start; else error with install hint
+ image aibox: exists? → build if missing (first run or post-update)
+ container exists?
+ ├─ no → create (docker run -d)
+ ├─ yes, stopped → docker start
+ └─ yes, running → (nothing)
+ container image ≠ aibox:?
+ → if no active exec sessions: recreate (rm + run; aibox-home and project
+ bind survive by construction; warn that apt-installed packages reset)
+ → if sessions active: warn and continue on the old image
+ proxy running? → start if not (§5)
+ docker exec -it ...
+```
+
+- One generic launcher (`cmd_run`) runs any program in the sandbox;
+ `aibox`/`aibox claude` dispatch to `run claude`. When the program is
+ claude, it is invoked with `--allow-dangerously-skip-permissions` (prompts
+ on, bypass selectable in-session) plus any passthrough args; `--yolo`
+ swaps that for `--dangerously-skip-permissions` (all prompts skipped) and
+ is rejected for other programs (their approval flags pass through as
+ normal args).
+- `--copy` runs claude in a disposable container instead: fresh container
+ per run (`-copy-`, no restart policy, labeled
+ `aibox.copy=1` with slug `-copy` for its own dev URLs), no bind
+ mount — the project is streamed in as a tar snapshot to the same absolute
+ path, extracted as the container user (so ownership is right without a
+ full-tree chown). It shares the home volume, so login and session history work,
+ and the container is removed on exit (EXIT trap; work is kept by
+ committing/pushing from inside).
+- **Nothing happens on session exit.** No idle-detection, no auto-stop, no
+ down. The container idles at ~zero CPU until the next attach or an
+ explicit `aibox stop`.
+- `aibox stop` = `docker stop` only. A stopped container preserves its
+ writable layer (apt installs etc.); next `aibox claude` starts it again in
+ ~1s.
+- The only code path that ever runs `docker rm` on a project container is
+ image-change recreation, which immediately recreates it.
+
+### 4.4 Image
+
+Dockerfile is embedded in the script (heredoc, as today) and written to
+`~/.aibox/Dockerfile` at build time:
+
+- `FROM node:${node_version}-bookworm` — verified to ship git, python3,
+ make, gcc/g++, and curl out of the box (~400 MB compressed, ~1.6 GB
+ uncompressed).
+- apt: `zsh sudo ripgrep fzf jq less procps curl` (keep this list short —
+ the container persists, so Claude apt-installs anything else once and it
+ sticks).
+- Claude Code installed via the native installer
+ (`curl -fsSL https://claude.ai/install.sh | bash`) **at first container
+ start** (entrypoint checks, installs if missing). The installer puts a
+ launcher at `~/.local/bin/claude` with versions under
+ `~/.local/share/claude/` — both inside the `aibox-home` volume — so
+ `claude update` (and the native install's background auto-updates)
+ persist across container recreation and image rebuilds, and all projects
+ share one install. Current Claude Code versions verifiably create
+ `.claude.json` *inside* `CLAUDE_CONFIG_DIR`, so all state lands in the
+ volume (very old builds handled `CLAUDE_CONFIG_DIR` inconsistently —
+ irrelevant here since the installer always fetches current). This is a hard constraint, not a
+ preference: v1 bakes the installer into the image's home dir
+ (`bin/aibox:556`), but in v2 the `aibox-home` volume mounts over all of
+ `/home/aibox`, shadowing anything the image put there — so nothing may be
+ installed into the home directory at image-build time.
+- The entrypoint also fixes ownership of `/home/aibox` on start (migrated
+ v1 volumes can contain root-owned files; v1 did the same for `.claude`,
+ `bin/aibox:572`).
+- Optional user extension: if `~/.aibox/Dockerfile.extra` exists, its
+ contents are appended to the generated Dockerfile before build. This is
+ the supported way to make custom tooling survive image-change recreations.
+- Build with `docker build --pull` so the `node:*-bookworm` base actually
+ refreshes on rebuilds (v1 never pulled, so its base only updated by
+ accident).
+
+### 4.5 State on the host
+
+```
+~/.aibox/
+ config # key=value, see §7
+ Dockerfile # generated at build (informational; regenerated each build)
+ Dockerfile.extra # optional, user-authored
+ Caddyfile # generated proxy config (§5)
+~/aibox-backups/ # default backup destination (§6)
+```
+
+`~/.config/aibox` (v1) is left untouched; the migration script may read it,
+the new CLI never does.
+
+## 5. Dev-server proxy
+
+Goal: Claude starts `vite` / `next dev` / `wrangler dev` on any port inside
+the container, and the host browser reaches it immediately at a stable URL —
+no commands, no restarts, no sidecars, no tunnels.
+
+- One shared container `aibox-proxy` (image: `caddy:2-alpine`), on the
+ `aibox` network, publishing `127.0.0.1:80->80`. Started lazily by the
+ first `aibox claude`/`shell`; stopped by `aibox stop --all`.
+- Routing rule: request `Host` of the form `..aibox.localhost`
+ reverse-proxies to `aibox--:` over the Docker network.
+ Because the proxy reaches containers directly on the internal network, **no
+ container ports are ever published** — any port works dynamically, ports
+ the app opens after container start included.
+- Config: a small generated Caddyfile (`~/.aibox/Caddyfile`) mounted into
+ the proxy. When a project container is created or renamed, the CLI
+ regenerates the file and reloads the proxy (`caddy reload` via exec —
+ graceful, in-flight connections drain rather than drop).
+- The core routing needs no per-project config at all — this exact
+ Caddyfile was tested against `caddy:2-alpine` (v2.11) and routes
+ `Host: 5173.myapp.aibox.localhost` to container `aibox-myapp:5173`
+ (labels index right-to-left from zero):
+
+ ```
+ http://*.*.aibox.localhost {
+ reverse_proxy aibox-{http.request.host.labels.2}:{http.request.host.labels.3}
+ }
+ ```
+
+ Per-project regeneration is only needed because real container names
+ carry the `-` suffix (§4.1) — a `map` block from slug to full
+ container name, rewritten on container create/rename.
+- WebSockets must work (Caddy's `reverse_proxy` handles them by default) —
+ vite HMR is the primary consumer.
+- `aibox status` and container-start output print the concrete base URL,
+ e.g. `http://5173.myapp.aibox.localhost`.
+- Inside the container, set an env var (e.g. `AIBOX_URL_BASE=myapp.aibox.localhost`)
+ so Claude can tell the user the right URL for whatever port it just opened.
+- If publishing host port 80 fails (already taken, or the runtime can't),
+ fall back to `proxy_port` from config (default fallback 8080) and include
+ the port in printed URLs (`http://5173.myapp.aibox.localhost:8080`).
+ Low-port caveat on macOS: binding `127.0.0.1:80` specifically needs
+ privileges — Docker Desktop handles it via its privileged helper, while
+ Colima/OrbStack emulate loopback-only publishing by binding `0.0.0.0` and
+ rejecting non-loopback sources (old Colima versions ignored the loopback
+ restriction entirely, exposing the port on the LAN — acceptable here
+ since everything behind it is already sandboxed dev traffic, but worth a
+ line in the README).
+- Non-goals: HTTPS (plain http on loopback is fine), public sharing
+ (Cloudflare tunnels remain possible manually; a built-in `aibox share` is
+ a future idea, §9), and CLI tools that don't respect `*.localhost` DNS
+ (curl needs `--resolve`; documented, not solved — `/etc/hosts` has no
+ wildcard support so there is no standard hosts-file fix).
+
+## 6. Backup and restore
+
+Design principle: a backup is one portable file; restore is dumb and
+predictable; nothing in either path can delete data it didn't just save.
+
+- `aibox backup [dir]`
+ - Snapshots the **entire `aibox-home` volume** (sessions, `.claude.json`,
+ credentials, shell history, claude binary, npm globals) to
+ `/aibox-home--.tar.gz`. Default dir:
+ `~/aibox-backups` (overridable in config).
+ - Implemented as a throwaway helper container mounting the volume
+ **read-only** and streaming `tar` to the host (the pattern Docker's own
+ docs recommend for volume backup). Safe to run while containers are up
+ in the sense that the source is never written to; a session actively
+ appending its `.jsonl` mid-tar may be captured mid-write, which is
+ acceptable for append-only session logs.
+ - Prints archive path + size; keeps every backup (no rotation in v2 — the
+ user deletes old ones; a `backup_keep` config knob is a future idea).
+- `aibox restore `
+ - `` is an archive produced by `aibox backup`.
+ - Steps: (1) require confirmation, (2) automatically run a safety backup
+ of the current volume first, (3) stop containers using the volume,
+ (4) wipe the volume and extract the archive into it, (5) restart what
+ was running — v2 instead REMOVES aibox containers after restore so the
+ next run recreates them against the restored layout (subpath mounts of a
+ pre-migration backup would not match).
+ - Restore is **replace**, not merge — merging heterogeneous histories is
+ the migration script's job (§6.1). This keeps restore trivially
+ predictable: after restore, the volume equals the backup, and the state
+ it replaced is itself a backup.
+
+### 6.1 Migration script (separate, one-time)
+
+`scripts/migrate-to-v2.sh` — lives in the repo, runs standalone (not part of
+the npm bin, never called by the CLI).
+
+Sources, all merged into the (possibly already-populated) `aibox-home`
+volume:
+
+1. **Every `aibox-auth-*` Docker volume** found on the machine (running or
+ stopped v1 containers don't matter; sources are only ever read).
+2. **Old backup folders** produced by the user's existing external backup
+ script, passed as arguments: `migrate-to-v2.sh [backup-dir ...]`.
+
+Merge rules:
+
+- Everything except `.claude.json`: copy the **entire volume tree
+ no-clobber** — don't enumerate directories (v1 volumes contain at least
+ `projects/`, `todos/`, `session-env/`, `file-history/`,
+ `shell-snapshots/`, per `bin/aibox:2047`, and the set may vary by Claude
+ Code version). Sessions are one file per UUID, so a plain union is
+ correct. Count copied vs skipped and report.
+- `.claude.json`: start from the newest copy (by mtime), then merge the
+ `projects` map keys from every other copy via `jq` (newest wins per key).
+ OAuth/credentials come from the newest copy only.
+- Session-key paths: default-mode v1 sessions are keyed by host paths (D6)
+ and need **no remapping**. Old `--copy`/`--worktree` sessions keyed under
+ `/workspace/...` are copied as-is (harmless), with an optional
+ `--map /workspace/foo=/Users/me/foo` flag to rekey them if ever wanted.
+- Idempotent: running it twice changes nothing the second time.
+- Strictly read-only on all sources. It never stops, deletes, or edits v1
+ containers, volumes, or backup folders — the user deletes those manually
+ after verifying (`docker volume rm`, etc.; the script prints the exact
+ cleanup commands as its final output but does not run them).
+
+## 7. Config
+
+`~/.aibox/config`, `key=value`, all optional:
+
+```
+node_version=24 # base image tag: node:-bookworm
+proxy_port=80 # host port for the dev-server proxy
+backup_dir=~/aibox-backups
+```
+
+No per-project config file in v2 (v1's `.aibox` is ignored). Changing
+`node_version` takes effect via `aibox update` (image rebuild → containers
+recreate on next start).
+
+## 8. Deleted from v1
+
+Removed entirely, with no deprecation shims — v2 is a clean break
+(major-version bump of `aibox-cli`):
+
+- Commands: `up`, `down`, `build`, `init`, `port-forward`, `volumes`,
+ `disk`, `clean`, `nuke`, `doctor`.
+- Flags: `-n/--name`, `-r/--repo`, `-b/--branch`, `-c/--copy`,
+ `-w/--worktree`, `-y/--yolo` and `-c/--copy` (v2 keeps only the long
+ `--yolo` / `--copy` spellings: claude's bypass-permissions flag and the
+ disposable snapshot container), `-s/--safe`,
+ `-i/--image`, `--all`/`--clean` on `down`.
+- Mechanisms: compose orchestration (v1 pipes generated YAML into
+ `docker compose -f -`) and the JetBrains-facing `compose.dev.yaml`, socat
+ port-forward sidecars, network firewall + `AIBOX_EXTRA_DOMAINS`,
+ restricted sudo, sensitive-file detection, WebStorm/JetBrains config
+ generation, IDE-integration plumbing (`~/.claude/ide` ro-mount and
+ `ENABLE_IDE_INTEGRATION`/`CLAUDE_CODE_SSE_PORT` forwarding), per-image
+ `aibox-auth-*` volumes, auto-`down` on last session exit, mode-switch
+ `down`, Colima/Docker auto-*install* (auto-*start* of an installed
+ Colima/OrbStack/Docker Desktop stays; installation becomes a printed
+ one-liner hint).
+
+Target size was **~500 lines** of bash for the core; the serve/sessions UIs
+(embedded Node, added post-spec) put the final file at ~1,700. If a CORE
+addition pushes past the ballpark,
+something from this spec is being over-built.
+
+## 9. Non-goals / future ideas
+
+Not in v2; recorded so they aren't accidentally half-built:
+
+- `aibox share ` — public URL via `cloudflared` quick tunnel (the
+ current manual workflow, automated).
+- Backup rotation (`backup_keep=N`).
+- Linux hosts as a first-class target (nothing should actively break, but
+ macOS + Colima/Docker Desktop/OrbStack is what gets tested).
+- Multiple containers per project, non-Claude agent presets, Homebrew tap
+ refresh.
+
+## 9b. Accepted implementation deviations
+
+Recorded post-implementation; intentional:
+
+- Image tag is `aibox:-node` (not bare
+ `aibox:`) so a `node_version` config change also triggers
+ rebuild + recreation by pure tag comparison.
+- Proxy slug-collision labels are computed over all project containers
+ (running or stopped), not just running ones — URLs stay stable across
+ stop/start.
+- `aibox status` shows the image only when it differs from current (as a
+ "will be recreated" note) rather than as a column.
+- Unstamped git checkouts run as version `dev` (image `aibox:dev-node`),
+ with the npm update notice disabled.
+
+## 10. Acceptance criteria
+
+The rewrite is done when all of these hold:
+
+1. `cd proj && aibox` on a fresh machine (Docker present): builds image,
+ creates volume/network/container/proxy, lands in a Claude session.
+2. Exit Claude, run `aibox claude` again → re-attached in under a second;
+ `apt install imagemagick` from a previous session is still installed.
+3. Two terminal tabs, same project: both `aibox claude` concurrently → two
+ sessions, one container, no race errors.
+4. `aibox stop && aibox claude` → container starts again with all state
+ (including apt layer) intact.
+5. Claude runs `npx vite` (port 5173) inside the container →
+ `http://5173..aibox.localhost` serves it in the host browser with
+ working HMR, with no port ever having been published or configured.
+6. `aibox update` after a version bump → next `aibox claude` recreates the
+ container on the new image; all Claude sessions, login, and `claude`
+ binary version are unchanged (home volume); a warning notes the apt
+ layer reset.
+7. `aibox backup` while a session is running → single `.tar.gz`;
+ `aibox restore` of it onto a wiped volume reproduces login + full
+ session list (`claude --resume` shows history).
+8. `scripts/migrate-to-v2.sh ` on a machine with existing
+ `aibox-auth-*` volumes → one `aibox-home` volume where `claude --resume`
+ in a previously-used project lists sessions that originated from *both*
+ the live volumes and the old backup folders; running it again reports
+ zero new files.
+9. Deleting any project container (`docker rm -f`) loses no Claude data —
+ next `aibox claude` recreates it and every session is still there.
+10. The core (excluding the embedded serve/sessions UI payloads) stays in
+ the ~500-line ballpark, and no command outside §3 exists.
diff --git a/bin/aibox b/bin/aibox
index 56c799e..4d502b7 100755
--- a/bin/aibox
+++ b/bin/aibox
@@ -1,2379 +1,1705 @@
#!/usr/bin/env bash
-# aibox - Run AI coding tools in isolated Docker containers.
+# aibox — persistent Docker sandboxes for Claude Code.
#
-# Usage:
-# aibox up Start the container
-# aibox claude [--yolo|--safe] Open Claude Code
-# [claude args] --yolo: skip permissions, full sudo, no firewall
-# --safe: keep permissions, restricted sudo, firewall on
-# Extra args are passed through to claude
-# (e.g. --resume, --print, --model)
-# aibox shell Open a zsh shell in the container
-# aibox shell Run a command in the container
-# aibox down Stop and remove the container
-# aibox down --clean Also remove copy volumes / worktrees
-# aibox down --all Stop all containers for this project
-# aibox port-forward PORT [PORT] Forward host port(s) to container
-# aibox port-forward --list List active port forwards
-# aibox port-forward --stop P Stop forwarding a port
-# aibox port-forward --stop-all Stop all port forwards
-# aibox status List all aibox containers
-# aibox volumes List copy volumes and worktrees
-# aibox disk Show disk usage breakdown
-# aibox clean [DAYS] Clean everything (default: 30 days)
-# --containers Only stopped containers
-# --volumes Only orphaned volumes
-# --docker Only dangling images + build cache
-# --sessions Only old Claude session data
-# aibox doctor Diagnose common issues
-# aibox nuke Remove ALL aibox containers (all projects)
-# aibox build [--image NAME] Build the base image
-# aibox init [flags] Generate compose.dev.yaml + configure WebStorm
-# aibox update Update aibox to the latest version
-# aibox version Show version
-# aibox help Show this help (default)
+# One container per project. One shared home volume (aibox-home) holds all
+# Claude state. Nothing is ever deleted without asking.
#
-# Flags:
-# -n, --name NAME Run a named instance (e.g. -n refactor)
-# Allows multiple containers per project
-# -d, --dir PATH Run in a different project directory
-# -r, --repo URL Clone a git repo and run in it
-# -b, --branch NAME Branch to checkout (with --repo)
-# -i, --image NAME Override base Docker image (default: aibox:latest)
-# -c, --copy Copy project into container (full isolation, no host sync)
-# -w, --worktree Use git worktree (lightweight isolation, stays on host)
-# -y, --yolo Yolo mode: skip permissions, full sudo, no firewall
-# -s, --safe Safe mode: keep permissions, restricted sudo, firewall on
-# --shared-modules Share node_modules between host and container (default)
-# --separate-modules Isolate node_modules in a Docker volume
-# --force Skip confirmation prompts (for clean)
+# Usage:
+# aibox [claude] [args...] Shorthand for `aibox run claude`: start/attach
+# this project's container and run Claude Code.
+# Permission prompts are on by default, with
+# bypass mode available in-session; pass --yolo
+# to skip all prompts from the start. Other args
+# pass to claude verbatim (--resume, -p,
+# --model, ...). `aibox --resume` works too.
+# aibox run [--copy] [args...]
+# Run any program in the container (e.g.
+# aibox run codex). With --copy (also on plain
+# aibox/claude), a disposable container on a
+# snapshot of the project is used instead — no
+# bind mount, the real directory can't be
+# touched, and the container is removed on exit
+# (commit & push from inside to keep work).
+# A `--` ends aibox's own flag parsing, so a
+# literal --copy/--yolo can reach the program.
+# aibox serve Run the sessions UI on port 45789 (dev-proxy
+# URL): start new phone/browser-drivable
+# sessions, resume past ones, see and stop live
+# ones. Each session is a detached claude
+# process in the container — it survives closing
+# the terminal and shows up at claude.ai / the
+# Claude app. `aibox serve stop` ends the UI and
+# every live session.
+# aibox sessions All projects' past sessions on one local page
+# (opens your browser; loopback-only, runs in
+# the foreground until Ctrl-C or its Close
+# button). Per session: open in a terminal, copy
+# the resume command, or make it phone-drivable.
+# aibox rc-resume Make one past session of this project
+# drivable from claude.ai / the Claude app.
+# aibox shell [cmd...] zsh in the container. One argument runs as a
+# shell string (pipes/globs work); several run
+# as a safely-quoted command.
+# aibox stop [--all] Stop this project's container
+# (--all: every aibox container + proxy).
+# Never deletes anything; next run re-attaches.
+# aibox status Containers with live memory + disk use,
+# dev-server URLs, Docker disk totals, home
+# volume size
+# aibox backup [dir] Snapshot the aibox-home volume to a tar.gz
+# (default dir: ~/aibox-backups)
+# aibox restore Restore a backup into aibox-home
+# (auto safety-backup of current state first)
+# aibox update Update the CLI; image rebuilds on next run
+# aibox version Show versions
+# aibox help Show this help
#
-# Network firewall:
-# Default: only allows Claude API, npm, GitHub, PyPI, SSH, DNS.
-# Add domains: export AIBOX_EXTRA_DOMAINS="example.com,api.myservice.io"
+# Dev servers: anything listening on any port inside the container is
+# reachable from the host browser at
+# http://..aibox.localhost
+# (Chrome/Edge/Firefox out of the box; Safari needs macOS 26+. No ports to
+# publish, no restarts — the proxy reaches the container over the Docker
+# network.) Inside the container, $AIBOX_URL_BASE holds
+# ".aibox.localhost[:port]", and exported ANTHROPIC_* host vars
+# are forwarded.
#
-# Prerequisites:
-# brew install colima docker docker-compose docker-buildx
-# (or: brew install orbstack)
+# Config (~/.aibox/config, key=value, all optional):
+# node_version=24 # base image: node:-bookworm
+# proxy_port=80 # host port for the dev-server proxy
+# backup_dir=~/aibox-backups
+# Extra image layers: ~/.aibox/Dockerfile.extra is appended to the generated
+# Dockerfile (survives image rebuilds; plain apt installs survive stop/start
+# but reset when the image changes).
#
-# First-time setup (once ever):
-# aibox build
+# All Claude state lives in one Docker volume (aibox-home), sliced per
+# project: login, settings, and the claude binary are shared; each project
+# gets a private home (ssh keys, shell history, its own sessions) that other
+# projects' containers never mount. Needs Docker Engine 26+. Volumes from
+# older aibox versions are migrated automatically (safety backup first).
+# Containers are disposable; the volume is what `aibox backup` protects.
#
-# Per-project:
-# aibox up # start default container
-# aibox claude # open Claude Code
-# aibox claude --yolo # skip permission prompts (sandboxed)
-# aibox --name feat claude # Claude Code in a second container
-# aibox shell # zsh in the container
-# aibox shell ls -la # run a command inline
-# aibox down # stop default
-# aibox down --all # stop all for this project
+# (This header is the output of `aibox help` — keep it accurate.)
set -euo pipefail
-
-# ── Script identity ──────────────────────────────────────────────
-SCRIPT_NAME="$(basename "$0")"
-AIBOX_VERSION="9"
-AIBOX_CLI_VERSION="__CLI_VERSION__"
-CONFIG_DIR="${HOME}/.config/aibox"
-DEFAULT_IMAGE="aibox:latest"
-CONTAINER_PREFIX="aibox"
-
-# ── Colors ────────────────────────────────────────────────────
-# Disable color if not a terminal or NO_COLOR is set
+unset CDPATH
+
+# Replaced by the release workflow; left as-is = running from a git
+# checkout, which behaves as version "dev".
+CLI_VERSION="__CLI_VERSION__"
+CONFIG_DIR="${HOME}/.aibox"
+VOLUME="aibox-home"
+NETWORK="aibox"
+PROXY="aibox-proxy"
+PROXY_IMAGE="caddy:2-alpine"
+HELPER_IMAGE="alpine:3.20"
+
+# ── Output helpers ───────────────────────────────────────────────
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
- _R='\033[0;31m' _BR='\033[1;31m'
- _Y='\033[0;33m' _BY='\033[1;33m'
- _G='\033[0;32m' _BG='\033[1;32m'
- _C='\033[0;36m' _BC='\033[1;36m'
- _M='\033[0;35m'
- _B='\033[1m' _D='\033[2m'
- _N='\033[0m'
+ _G='\033[1;32m' _Y='\033[1;33m' _R='\033[1;31m' _D='\033[2m' _B='\033[1m' _N='\033[0m'
else
- _R='' _BR='' _Y='' _BY='' _G='' _BG='' _C='' _BC='' _M='' _B='' _D='' _N=''
+ _G='' _Y='' _R='' _D='' _B='' _N=''
fi
-
-# ── Output helpers ─────────────────────────────────────────────
-_step() {
- local msg="$1"
- local width=60
- local pad
- pad=$(( width - ${#msg} - 5 )) # 5 = "── " + " ─"
- [[ $pad -lt 3 ]] && pad=3
- local line
- line=$(printf '─%.0s' $(seq 1 "$pad"))
- echo ""
- echo -e "${_BC}── ${_B}${msg}${_N}${_BC} ${line}${_N}"
- echo ""
-}
-
-_ok() { echo -e " ${_BG}✓${_N} $*"; }
-_warn() { echo -e " ${_BY}⚠${_N} ${_BY}$1${_N}"; shift || true; if [[ $# -gt 0 ]]; then for line in "$@"; do echo -e " $line"; done; fi; }
-_err() { echo -e " ${_BR}✗ Error:${_N} $*" >&2; }
-_info() { echo -e " ${_D}ℹ${_N} $*"; }
-_file() { echo -e "${_C}$1${_N}"; }
-
-# ── Machine-aware Colima start ───────────────────────────────────
-_colima_start() {
- local total_cpu total_mem_gb vm_cpu vm_mem vm_disk
-
- # Detect machine resources
- if [[ "$(uname)" == "Darwin" ]]; then
- total_cpu=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
- total_mem_gb=$(( $(sysctl -n hw.memsize 2>/dev/null || echo 8589934592) / 1073741824 ))
- else
- total_cpu=$(nproc 2>/dev/null || echo 4)
- total_mem_gb=$(( $(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}' || echo 8388608) / 1048576 ))
- fi
-
- # Allocate half CPU (min 2, max 4), half RAM (min 4GB, max 8GB), 100GB disk
- vm_cpu=$(( total_cpu / 2 ))
- [[ $vm_cpu -lt 2 ]] && vm_cpu=2
- [[ $vm_cpu -gt 4 ]] && vm_cpu=4
- vm_mem=$(( total_mem_gb / 2 ))
- [[ $vm_mem -lt 4 ]] && vm_mem=4
- [[ $vm_mem -gt 8 ]] && vm_mem=8
- vm_disk=100
-
- echo "Detected: ${total_cpu} CPUs, ${total_mem_gb}GB RAM"
- echo "Colima VM: ${vm_cpu} CPUs, ${vm_mem}GB RAM, ${vm_disk}GB disk"
-
- # Try Apple Virtualization framework first (fast), fall back to QEMU
- colima start \
- --cpu "$vm_cpu" --memory "$vm_mem" --disk "$vm_disk" \
- --vm-type vz --vz-rosetta 2>/dev/null \
- || colima start \
- --cpu "$vm_cpu" --memory "$vm_mem" --disk "$vm_disk"
-}
-
-# ── Dependency check ─────────────────────────────────────────────
-_check_deps() {
- local missing=()
-
- command -v docker &>/dev/null || missing+=("docker")
- command -v docker-compose &>/dev/null || docker compose version &>/dev/null || missing+=("docker-compose")
-
- if [[ ${#missing[@]} -eq 0 ]]; then
- # Docker CLI exists — check if daemon is reachable
- if ! docker info &>/dev/null; then
- if command -v colima &>/dev/null; then
- echo "Docker daemon not running. Starting Colima..."
- _colima_start
- elif [[ -d "/Applications/OrbStack.app" ]]; then
- echo "Docker daemon not running. Starting OrbStack..."
- open -a OrbStack
- local i=0
- while ! docker info &>/dev/null && [[ $i -lt 30 ]]; do
- sleep 1
- i=$((i + 1))
- done
- if ! docker info &>/dev/null; then
- echo "OrbStack started but Docker daemon not ready. Try again in a moment." >&2
- exit 1
- fi
- elif [[ -d "/Applications/Docker.app" ]]; then
- echo "Docker daemon not running. Starting Docker Desktop..."
- open -a Docker
- local i=0
- while ! docker info &>/dev/null && [[ $i -lt 60 ]]; do
- sleep 1
- i=$((i + 1))
- done
- if ! docker info &>/dev/null; then
- echo "Docker Desktop started but daemon not ready. Try again in a moment." >&2
- exit 1
- fi
- else
- echo "Docker daemon not running. Install Docker Desktop, Colima, or OrbStack." >&2
- exit 1
- fi
- fi
- return
- fi
-
- # Something is missing — offer to install
- if ! command -v brew &>/dev/null; then
- echo "Homebrew is required to install Docker dependencies."
- echo "Will run: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
- echo ""
- if _confirm_yes "Install Homebrew?"; then
- /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- if [[ -f /opt/homebrew/bin/brew ]]; then
- eval "$(/opt/homebrew/bin/brew shellenv)"
- elif [[ -f /usr/local/bin/brew ]]; then
- eval "$(/usr/local/bin/brew shellenv)"
- fi
- if ! command -v brew &>/dev/null; then
- echo "Homebrew installed but not in PATH. Open a new terminal and re-run ${SCRIPT_NAME}." >&2
- exit 1
- fi
- else
- echo "Aborted. Install Homebrew (https://brew.sh) then re-run ${SCRIPT_NAME}." >&2
- exit 1
- fi
- fi
-
- echo "Missing dependencies: ${missing[*]}"
- echo "Will install via Homebrew: colima docker docker-compose docker-buildx"
- echo ""
- if _confirm_yes "Install now?"; then
- brew install colima docker docker-compose docker-buildx
-
- mkdir -p ~/.docker/cli-plugins
- ln -sfn "$(brew --prefix)/opt/docker-compose/bin/docker-compose" ~/.docker/cli-plugins/docker-compose
-
- echo ""
- echo "Starting Colima..."
- _colima_start
- echo "Docker is ready."
- else
- echo "Aborted. Install manually:" >&2
- echo " brew install colima docker docker-compose docker-buildx" >&2
- exit 1
- fi
-}
-
-# ── Parse global flags ───────────────────────────────────────────
-IMAGE="$DEFAULT_IMAGE"
-SHARED_MODULES=true
-INSTANCE_NAME=""
-PROJECT_DIR_FLAG=""
-REPO_URL=""
-REPO_BRANCH=""
-DOWN_ALL=false
-DOWN_CLEAN=false
-FORCE_CLEAN=false
-SKIP_PERMISSIONS=false
-SAFE_MODE=false
-ISOLATION=""
-POSITIONAL=()
-
-parse_flags() {
- while [[ $# -gt 0 ]]; do
- case "$1" in
- -i|--image)
- IMAGE="${2:?'--image requires a value'}"
- shift 2
- ;;
- -n|--name)
- INSTANCE_NAME="${2:?'--name requires a value'}"
- shift 2
- ;;
- -d|--dir)
- PROJECT_DIR_FLAG="${2:?'--dir requires a value'}"
- shift 2
- ;;
- -r|--repo)
- REPO_URL="${2:?'--repo requires a value'}"
- shift 2
- ;;
- -b|--branch)
- REPO_BRANCH="${2:?'--branch requires a value'}"
- shift 2
- ;;
- --shared-modules)
- SHARED_MODULES=true
- shift
- ;;
- --separate-modules)
- SHARED_MODULES=false
- shift
- ;;
- -c|--copy)
- [[ -n "$ISOLATION" ]] && { echo "Error: --copy and --worktree are mutually exclusive" >&2; exit 1; }
- ISOLATION="copy"
- shift
- ;;
- -w|--worktree)
- [[ -n "$ISOLATION" ]] && { echo "Error: --copy and --worktree are mutually exclusive" >&2; exit 1; }
- ISOLATION="worktree"
- shift
- ;;
- --all)
- DOWN_ALL=true
- shift
- ;;
- --clean)
- DOWN_CLEAN=true
- shift
- ;;
- --force)
- FORCE_CLEAN=true
- shift
- ;;
- -y|--yolo)
- SKIP_PERMISSIONS=true
- shift
- ;;
- -s|--safe)
- SAFE_MODE=true
- shift
- ;;
- *)
- POSITIONAL+=("$1")
- shift
- ;;
+_ok() { echo -e "${_G}✓${_N} $*"; }
+_info() { echo -e "${_D}·${_N} $*"; }
+_warn() { echo -e "${_Y}⚠${_N} $*"; }
+_die() { echo -e "${_R}✗${_N} $*" >&2; exit 1; }
+
+# ── Config ───────────────────────────────────────────────────────
+NODE_VERSION=24
+PROXY_PORT=80
+BACKUP_DIR="${HOME}/aibox-backups"
+if [[ -f "${CONFIG_DIR}/config" ]]; then
+ # `|| [[ -n "$k" ]]`: don't drop the last line when the file has no
+ # trailing newline.
+ while IFS='=' read -r k v || [[ -n "$k" ]]; do
+ [[ "$k" == \#* || -z "$k" || -z "$v" ]] && continue
+ v="${v/#\~/$HOME}"
+ case "$k" in
+ node_version) NODE_VERSION="$v" ;;
+ proxy_port) PROXY_PORT="$v" ;;
+ backup_dir) BACKUP_DIR="$v" ;;
esac
- done
-}
-
-parse_flags "$@"
-if [[ ${#POSITIONAL[@]} -gt 0 ]]; then
- set -- "${POSITIONAL[@]}"
-else
- set --
-fi
-
-# Set container mode from flags (cmd_claude may override via interactive prompt)
-if [[ "$SKIP_PERMISSIONS" == "true" ]]; then
- export AIBOX_MODE="yolo"
-elif [[ "$SAFE_MODE" == "true" ]]; then
- export AIBOX_MODE="safe"
-fi
-
-# ── Repo clone ───────────────────────────────────────────────────
-if [[ -n "$REPO_URL" ]]; then
- # Derive a directory name from the repo URL (hash prevents collisions)
- _repo_name=$(basename "$REPO_URL" .git)
- _repo_hash=$(printf '%s' "$REPO_URL" | shasum | cut -c1-6)
- _repo_dir="${CONFIG_DIR}/repos/${_repo_name}-${_repo_hash}"
-
- if [[ -d "$_repo_dir/.git" ]]; then
- echo "Repo already cloned (${_repo_dir}). Reusing."
- # Fetch latest and checkout branch if specified
- if [[ -n "$REPO_BRANCH" ]]; then
- git -C "$_repo_dir" fetch --all --quiet 2>/dev/null || true
- git -C "$_repo_dir" checkout "$REPO_BRANCH" 2>/dev/null || true
- fi
- else
- echo "Cloning ${REPO_URL}..."
- mkdir -p "${CONFIG_DIR}/repos"
- _clone_args=(--recursive)
- [[ -n "$REPO_BRANCH" ]] && _clone_args+=(--branch "$REPO_BRANCH")
- git clone "${_clone_args[@]}" "$REPO_URL" "$_repo_dir"
- fi
-
- PROJECT_DIR_FLAG="$_repo_dir"
-fi
-
-# ── Per-project config file (.aibox) ─────────────────────────────
-if [[ -n "$PROJECT_DIR_FLAG" ]]; then
- PROJECT_DIR="$(cd "$PROJECT_DIR_FLAG" 2>/dev/null && pwd)" || {
- echo "Error: directory not found: $PROJECT_DIR_FLAG" >&2
- exit 1
- }
-else
- PROJECT_DIR="$(pwd)"
+ done < "${CONFIG_DIR}/config"
fi
-PROJECT_CONF="${PROJECT_DIR}/.aibox"
-
-# Git root may differ from PROJECT_DIR (subfolder of a repo)
-GIT_ROOT=$(git -C "$PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
+mkdir -p "$CONFIG_DIR"
-# ── Safety: refuse to run in dangerous directories ───────────────
-_is_safe_project_dir() {
- local dir="$1"
- case "$dir" in
+# ── Derived names ────────────────────────────────────────────────
+PROJECT_DIR="$(pwd -P)"
+SLUG="$(basename "$PROJECT_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g; s/_/-/g')"
+HASH6="$(printf '%s' "$PROJECT_DIR" | { sha256sum || shasum -a 256; } 2>/dev/null | cut -c1-6)"
+# Name format aibox-- is load-bearing: _proxy_pairs extracts
+# the trailing hash6 with ${name##*-} for slug-collision URLs.
+CONTAINER="aibox-${SLUG}-${HASH6}"
+VERSION_TAG="$CLI_VERSION"
+[[ "$VERSION_TAG" == "__CLI_VERSION__" ]] && VERSION_TAG="dev"
+# VERSION_TAG also names backups (aibox-home--