release: box 0.5.0 — codex+grok templates, migrate-host, box expose #56

Merged
dan-claude-bot merged 23 commits from integration/0.5.0 into main 2026-07-15 00:04:54 +00:00
8 changed files with 90 additions and 36 deletions
Showing only changes of commit 455fbc656e - Show all commits

View file

@ -18,7 +18,7 @@ live in a shared profile no template can touch, so `blank` is a box with
nobody home — not a box with the safety off.
**The tool knows nothing about your projects.** You just `git clone` inside a
box. A repo can ship an optional [`.claudebox/`](docs/box-recipe.md)
box. A repo can ship an optional [`.box/`](docs/box-recipe.md)
runbook that Claude Code reads and acts on — there is no `install` step and no
host-run setup. See [docs/box-design.md](docs/box-design.md) for the
design rationale.
@ -44,7 +44,7 @@ retires the old `claudebox` symlink. (No `git clone` needed.)
## One-time host setup (Ubuntu 24.04 / Debian 13)
```sh
~/.local/share/box/host/setup-host.sh # run twice if it adds you to incus-admin (re-login between)
box setup-host # run twice if it adds you to incus-admin (re-login between)
```
Idempotent. Installs Incus and creates the isolation stack: the `boxnet` NAT
@ -70,7 +70,7 @@ claude # then run /login — copy the URL (press c), o
# in YOUR browser, paste the code back. No host CLI needed.
gh auth login # or drop a PAT in — your git credentials, your call
git clone https://github.com/you/project && cd project
claude # if the repo has .claudebox/, Claude reads it and sets up
claude # if the repo has .box/, Claude reads it and sets up
```
## Templates
@ -158,7 +158,8 @@ dev/test only.
## Boxes are just Incus instances
A box is an ordinary Incus instance tagged `user.box=1` (pre-0.4.0 boxes
carry `user.claudebox=1`, honored forever). box wraps the box lifecycle and the isolation model — not all of Incus. It owns a command
carry `user.claudebox=1`, honored forever). box wraps the box lifecycle and
the isolation model — not all of Incus. It owns a command
when it must enforce something Incus can't see: that tag (it will not stop,
rename or delete an instance it didn't mint), the isolation stack, or the
creds-free snapshot workflow. For everything else, there's the door:
@ -227,9 +228,9 @@ diagnoses the host faults that have actually happened: a wedged Incus daemon,
a dnsmasq that silently isn't serving, a VPN resolver that boxes would
inherit.
## Recipes: the `.claudebox/` convention
## Recipes: the `.box/` convention
A repo that wants to be easy to stand up in a box ships an optional `.claudebox/`
A repo that wants to be easy to stand up in a box ships an optional `.box/`
folder — a runbook Claude reads and follows (install deps, start services,
template env, seed data, smoke-test). It is agent-facing documentation, not a
host-executed script. See [docs/box-recipe.md](docs/box-recipe.md).
@ -237,9 +238,9 @@ host-executed script. See [docs/box-recipe.md](docs/box-recipe.md).
## Uninstall
```sh
~/.local/share/box/host/teardown-host.sh # boxes, network, ACL, profile, firewall
~/.local/share/box/host/teardown-host.sh --purge-incus # ...and Incus itself
rm -rf ~/.local/share/box ~/.local/bin/box # the CLI
box teardown-host # boxes, network, ACL, profile, firewall
box teardown-host --purge-incus # ...and Incus itself
rm -rf ~/.local/share/box ~/.local/bin/box # the CLI itself
```
## Non-goals

51
bin/box
View file

@ -53,6 +53,9 @@ CMDS=(
"expose^<box> <port> [<host-port>] | --list | --remove <port>^box^Forward a box port to the host's loopback — see a dev server^fn:cmd_expose^"
"incus^<box> -- <args...>^box^Escape hatch: run any incus command against a box^fn:cmd_incus^"
"doctor^[--fix | --pin-dns]^^Is this host fit to mint boxes? Diagnose the daemon, network, DNS, isolation^fn:cmd_doctor^"
"setup-host^^^One-time host setup: Incus, the boxnet stack, the profile, the firewall^fn:cmd_setup_host^"
"teardown-host^[--purge-incus]^^Remove the box host stack (both name generations)^fn:cmd_teardown_host^"
"migrate-host^--box <n> | --all-boxes | --retire-legacy^^Move a host from the pre-0.4.0 stack onto box^fn:cmd_migrate_host^"
"status^^^Deprecated alias for 'list'^fn:cmd_status^"
"help^[<command>]^^This help, or 'box help <command>' for one command^fn:cmd_help^"
)
@ -119,7 +122,7 @@ EOF
local r v sum
for r in "${CMDS[@]}"; do
IFS='^' read -r v _ _ sum _ _ <<<"$r"
printf ' %-9s %s\n' "$v" "$sum"
printf ' %-13s %s\n' "$v" "$sum"
done
cat <<'EOF'
@ -371,6 +374,37 @@ cloud-init error that names none of them.
Exit 0 = clean; 1 = problems found (each printed with its fix). Read-only
unless --fix or --pin-dns is given.
EOF
;;
setup-host) cat <<'EOF'
Prepare this host to mint boxes — one time. Installs Incus and builds the
isolation stack: the boxnet NAT bridge (resolver pinned), the box-isolate
ACL, the box-net profile, and the firewall rules, all re-applied at boot.
Idempotent — safe to re-run after a box upgrade to pick up stack changes.
If it has to add you to the incus-admin group it will say so and exit; log
back in (or 'sg incus-admin') and run it again.
box setup-host
EOF
;;
teardown-host) cat <<'EOF'
Remove the box host stack — all boxes, the boxnet/claudenet networks, the
ACLs, the profiles, and the firewall rules of BOTH name generations (current
and pre-0.4.0). Asks first.
box teardown-host # the stack; leaves Incus installed
box teardown-host --purge-incus # ...and uninstall Incus too
EOF
;;
migrate-host) cat <<'EOF'
Move a host from the pre-0.4.0 'claudebox' stack onto 'box'. Re-homing
preserves a box's authed state (no re-login) — it only re-tags and reassigns
the profile, then verifies the box works on its new network leg.
box migrate-host --box <name> # re-home one legacy box
box migrate-host --all-boxes # re-home every legacy box
box migrate-host --retire-legacy # remove the old stack (once no legacy box remains)
EOF
;;
status) cat <<'EOF'
Deprecated alias for 'box list'. It ignored the <box> argument it
@ -424,6 +458,8 @@ while [ $# -gt 0 ]; do
if [ "$cmd" = doctor ]; then args+=("$1"); shift; continue; fi
# expose's own flags (--list, --remove) are positional to it, not box's
if [ "$cmd" = expose ]; then args+=("$1"); shift; continue; fi
# the host verbs delegate their flags to the scripts they wrap
case "$cmd" in setup-host|teardown-host|migrate-host) args+=("$1"); shift; continue ;; esac
if [ "$cmd" = exec ] || [ "$cmd" = incus ]; then
usage_error "unknown option: $1 — a command's own flags go after --, as in '$(synopsis_of "$cmd")'"
fi
@ -843,6 +879,19 @@ cmd_doctor() {
exec bash "$script" "${args[@]}"
}
# The host lifecycle scripts, as first-class verbs — nobody should have to know
# where the install tree keeps its scripts. Each execs the installed script
# with its flags passed through; the script owns its own behavior (setup-host's
# incus-admin re-login dance, teardown's confirmation, migrate's per-box work).
host_script() { # $1 = script basename under host/
local script="$root/host/$1"
[ -f "$script" ] || die "$1 not found at $script — re-run install.sh"
exec bash "$script" "${args[@]}"
}
cmd_setup_host() { host_script setup-host.sh; }
cmd_teardown_host() { host_script teardown-host.sh; }
cmd_migrate_host() { host_script migrate-host.sh; }
cmd_help() { show_help "${args[0]:-}"; }
# The escape hatch. The box is resolved and tag-checked; everything else is

View file

@ -10,7 +10,7 @@ design decisions.
- **The tool** mints isolated boxes with Claude installed but **unauthenticated**.
It knows nothing about projects, secrets, recipes, or memory.
- **The agent** (Claude Code, inside the box) reads an optional `.claudebox/`
- **The agent** (Claude Code, inside the box) reads an optional `.box/`
runbook in a cloned repo and acts on it. The recipe's consumer is the
reasoning agent, not host machinery.
@ -45,10 +45,10 @@ Log in once → snapshot → spin up authed boxes from it.
cloud-init installs a global `~/.claude/CLAUDE.md` in every box telling Claude it
is running in a box (trust-less, ephemeral, creds-free) and to treat a
repo's `.claudebox/` folder as its bootstrap runbook. No "tell it" step, no host
repo's `.box/` folder as its bootstrap runbook. No "tell it" step, no host
execution.
## `.claudebox/` is optional, agent-facing documentation
## `.box/` is optional, agent-facing documentation
Not host-executed shell. A repo that wants to be easy to stand up in a sandbox
ships a runbook (prose + optional scripts the agent may run). A repo that does
@ -56,12 +56,12 @@ not, you set up by hand. The tool enforces no contract; there is no `install`.
## What box owns, and what it doesn't
Boxes are ordinary Incus instances, tagged `user.claudebox=1`. That makes every
Boxes are ordinary Incus instances, tagged `user.box=1`. That makes every
Incus verb a candidate feature request — `rename`, `info`, `file push`, on
forever — and wrapping them one at a time grows a worse `incus`. The rule:
> **box owns a command when it must enforce an invariant Incus cannot see:**
> the `user.claudebox=1` boundary (never touch an instance we didn't mint), the
> the `user.box=1` boundary (never touch an instance we didn't mint), the
> isolation stack (`claude-dev` profile + `claudenet` + ACL), or the creds-free
> snapshot→clone workflow. Everything else is Incus's job.

View file

@ -1,4 +1,4 @@
# The `.claudebox/` convention
# The `.box/` convention
`box` mints trust-less, creds-free, isolated VMs with Claude Code already
installed (`box new/shell/snapshot/restore/exec/down/start/rm/status`). The
@ -6,13 +6,17 @@ tool knows **nothing** about your project. There is no `install` step and no
host-run setup script.
A project makes itself easy to stand up inside a box by shipping an optional
`.claudebox/` folder. This folder is **agent-facing documentation** — read and
`.box/` folder. This folder is **agent-facing documentation** — read and
acted on by Claude Code (the reasoning agent) running inside the box. It is not
shell that the host executes.
> The folder was named `.claudebox/` before the 0.5.0 rename. Repos that still
> ship `.claudebox/` keep working — the agent is told to read either — but new
> projects should use `.box/`.
## What it is / what it is not
- **Optional.** No `.claudebox/` is a perfectly valid state.
- **Optional.** No `.box/` is a perfectly valid state.
- **Agent-facing.** You are writing instructions to a reasoning agent, not a
machine. Prose is fine; the agent adapts.
- **Not a host contract.** The host never parses, sources, or runs anything in
@ -23,34 +27,34 @@ shell that the host executes.
## How it's consumed
Every box ships a global `~/.claude/CLAUDE.md` telling Claude it is inside a
box and to treat a repo's `.claudebox/` folder as its bootstrap runbook.
box and to treat a repo's `.box/` folder as its bootstrap runbook.
So the whole flow is:
```
box new # get a box
box shell # get in
git clone <repo> && cd <repo>
claude # Claude reads .claudebox/ and brings the project up
claude # Claude reads .box/ and brings the project up
```
The operator can also just say: *"set this project up per .claudebox"*.
The operator can also just say: *"set this project up per .box"*.
## Suggested contents (all optional)
Author everything here for a reasoning agent.
- **`.claudebox/SETUP.md`** — the prose runbook. Prerequisites, how to install
- **`.box/SETUP.md`** — the prose runbook. Prerequisites, how to install
deps, how to start services, how to template the env, how to seed data, and
how to smoke-test. Written as instructions to Claude.
- **Helper scripts** (e.g. `.claudebox/dev-up.sh`) that the runbook tells Claude
- **Helper scripts** (e.g. `.box/dev-up.sh`) that the runbook tells Claude
to run. Claude decides to run them; the host never does.
- **`.claudebox/env.template`** — example env the runbook explains how to fill.
- **`.box/env.template`** — example env the runbook explains how to fill.
Staging values the operator pastes in. **Never commit real secrets.**
- **`.claudebox/compose.yml`** — optional services the runbook starts.
- **`.box/compose.yml`** — optional services the runbook starts.
## Worked example
A minimal `.claudebox/SETUP.md` for a Node + Postgres app:
A minimal `.box/SETUP.md` for a Node + Postgres app:
```markdown
# Setup
@ -58,8 +62,8 @@ A minimal `.claudebox/SETUP.md` for a Node + Postgres app:
This is a Node service backed by Postgres.
1. Install deps: `npm ci`
2. Start Postgres: `docker compose -f .claudebox/compose.yml up -d`
3. Create the env file: copy `.claudebox/env.template` to `.env` and ask the
2. Start Postgres: `docker compose -f .box/compose.yml up -d`
3. Create the env file: copy `.box/env.template` to `.env` and ask the
operator to fill in `DATABASE_URL` and `API_KEY` (staging values).
4. Run migrations: `npm run migrate`
5. Start the app: `npm run dev`
@ -72,7 +76,7 @@ That's it — Claude reads it top to bottom and adapts if reality differs.
- **Keep it declarative and resilient.** State intent and steps; let the agent
adapt when the repo has drifted. Don't hard-code brittle assumptions.
- **Never put real credentials in `.claudebox/`.** Templates and staging
- **Never put real credentials in `.box/`.** Templates and staging
placeholders only. The operator pastes real values at runtime.
- **No `.claudebox/` is fine.** The operator can stand the project up by hand,
- **No `.box/` is fine.** The operator can stand the project up by hand,
or let Claude infer the steps from the repo's `README` / `CLAUDE.md`.

View file

@ -49,7 +49,7 @@ if incus network show boxnet >/dev/null 2>&1; then
ok "dns.mode = none — a box cannot enumerate its siblings by name"
else
no "dns.mode = ${dns:-<unset>} — a box can RESOLVE its siblings' names and addresses"
inf "fix: re-run ~/.local/share/box/host/setup-host.sh"
inf "fix: re-run: box setup-host"
[ "$FIX" = 1 ] && { incus network set boxnet dns.mode=none && inf "set: dns.mode=none"; }
fi
inf "ipv4.address = $(incus network get boxnet ipv4.address 2>/dev/null)"
@ -110,7 +110,7 @@ if [ -n "$PROFILES" ]; then
no "$p: security.port_isolation is NOT set — BOXES CAN REACH EACH OTHER"
inf "an L3 ACL cannot do this: two boxes on one bridge are on the same L2"
inf "segment, so their frames are switched, never routed past the ACL."
inf "fix: re-run ~/.local/share/box/host/setup-host.sh"
inf "fix: re-run: box setup-host"
fi
for k in security.mac_filtering security.ipv4_filtering; do
v="$(incus profile device get "$p" eth0 "$k" 2>/dev/null)"
@ -192,7 +192,7 @@ else
no "the host resolves via a CGNAT/Tailscale resolver ($hostns), and boxes INHERIT it — see issue #33"
inf "· box DNS breaks whenever the tailnet's resolver does (this is what kills cold mints)"
inf "· and tailnet names RESOLVE from inside a box, though its ACL blocks connecting to them"
inf "fix: re-run ~/.local/share/box/host/setup-host.sh (it pins the resolver)"
inf "fix: re-run: box setup-host (it pins the resolver)"
inf " or quick-test the pin alone: bash drill/doctor.sh --pin-dns"
else
inf "boxes inherit the host's resolver (unpinned — setup-host.sh pins this now; re-run it)"

View file

@ -25,7 +25,7 @@ write_files:
- **Disposable.** Nothing here is backed up. State is discarded when the box is
removed; the operator persists work via git push and via `box snapshot`.
- **Bootstrap runbook.** If the repository you are working in contains a
`.claudebox/` folder, read it as your setup runbook — how to install
`.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install
dependencies, start services, template environment files, seed data, and
smoke-test — and follow it. It is documentation for you, not a script the
host runs.

View file

@ -26,7 +26,7 @@ write_files:
- **Disposable.** Nothing here is backed up. State is discarded when the box
is removed; the operator persists work via git push and via `box snapshot`.
- **Bootstrap runbook.** If the repository you are working in contains a
`.claudebox/` folder, read it as your setup runbook — how to install
`.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install
dependencies, start services, template environment files, seed data, and
smoke-test — and follow it. It is documentation for you, not a script the
host runs.

View file

@ -26,7 +26,7 @@ write_files:
- **Disposable.** Nothing here is backed up. State is discarded when the box
is removed; the operator persists work via git push and via `box snapshot`.
- **Bootstrap runbook.** If the repository you are working in contains a
`.claudebox/` folder, read it as your setup runbook — how to install
`.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install
dependencies, start services, template environment files, seed data, and
smoke-test — and follow it. It is documentation for you, not a script the
host runs.