chore: finish the debrand — env vars, install dir, docs are 'box', not 'claudebox'

The 0.4.0 rename left surface leftovers the user hit through the
CLAUDEBOX_* env vars. Sweep them, drawing a clean line:

  box       = everything the user touches — env vars (BOX_REPO/REF/HOME/
              BIN), installer messages (box-install:), the install tree
              (~/.local/share/box, with the installer sweeping the old
              ~/.local/share/claudebox on upgrade), tool prose, and the
              docs (docs/box-{design,recipe}.md).
  claudebox = the GitHub repo name (URLs, the claudebox-<ref> tarball
              dir, issue refs), the legacy user.claudebox=1 tag, the
              old-stack cleanup code (claudenet/claude-dev/claude-isolate/
              claudebox-firewall), and the .claudebox/ runbook convention
              — a deliberate v1 hold, since renaming it breaks consuming
              repos.

Renamed the two doc files and their links; updated drill.sh/doctor.sh
paths and BOX_REPO/BOX_REF; fixed the claude template's in-box briefing
to say 'box'. RUNS.md left as-is (append-only history). No behavior
change beyond the install-dir move, which the installer migrates.
This commit is contained in:
claude-hdb 2026-07-14 17:44:24 +00:00
parent 912e0621ca
commit 4eb6b35a7b
9 changed files with 75 additions and 66 deletions

View file

@ -18,9 +18,9 @@ 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/claudebox-recipe.md)
box. A repo can ship an optional [`.claudebox/`](docs/box-recipe.md)
runbook that Claude Code reads and acts on — there is no `install` step and no
host-run setup. See [docs/claudebox-design.md](docs/claudebox-design.md) for the
host-run setup. See [docs/box-design.md](docs/box-design.md) for the
design rationale.
> **0.4.0 is a clean cut**: the CLI is `box` (no `claudebox` shim), the host
@ -37,14 +37,14 @@ design rationale.
curl -fsSL https://raw.githubusercontent.com/heavy-duty/claudebox/main/install.sh | bash
```
Installs the tree to `~/.local/share/claudebox` and links `box` onto your
Installs the tree to `~/.local/share/box` and links `box` onto your
`PATH`. Re-run any time to upgrade — upgrading from a pre-0.4.0 install also
retires the old `claudebox` symlink. (No `git clone` needed.)
## One-time host setup (Ubuntu 24.04 / Debian 13)
```sh
~/.local/share/claudebox/host/setup-host.sh # run twice if it adds you to incus-admin (re-login between)
~/.local/share/box/host/setup-host.sh # run twice if it adds you to incus-admin (re-login between)
```
Idempotent. Installs Incus and creates the isolation stack: the `boxnet` NAT
@ -172,7 +172,7 @@ The box is resolved and tag-checked; the rest is passed to `incus` verbatim, and
the command is echoed before it runs. If it can move the box off the isolation
stack (profile, network, device, `security.*`), box warns and proceeds —
the trust boundary is then yours to keep. See
[docs/claudebox-design.md](docs/claudebox-design.md) for the rule and why the
[docs/box-design.md](docs/box-design.md) for the rule and why the
command surface is a table.
## Isolation
@ -232,14 +232,14 @@ inherit.
A repo that wants to be easy to stand up in a box ships an optional `.claudebox/`
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/claudebox-recipe.md](docs/claudebox-recipe.md).
host-executed script. See [docs/box-recipe.md](docs/box-recipe.md).
## Uninstall
```sh
~/.local/share/claudebox/host/teardown-host.sh # boxes, network, ACL, profile, firewall
~/.local/share/claudebox/host/teardown-host.sh --purge-incus # ...and Incus itself
rm -rf ~/.local/share/claudebox ~/.local/bin/box # the CLI
~/.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
```
## Non-goals

View file

@ -1,6 +1,6 @@
# claudebox design
# box design
`claudebox` is a CLI that mints and manages **trust-less, network-isolated VMs
`box` is a CLI that mints and manages **trust-less, network-isolated VMs
with Claude Code installed**. It is infrastructure, not a project provisioner.
See issue #3 for the full reframe and rationale. This doc captures the durable
@ -16,7 +16,7 @@ design decisions.
## Boxes are strictly creds-free
`claudebox new --name <n>` launches a blank box: everything installed, **no**
`box new --name <n>` launches a blank box: everything installed, **no**
git credentials and **no** Claude credentials. The operator authenticates
interactively *inside* the box:
@ -33,18 +33,18 @@ multi-user problem: nothing shared, nothing committed.
Re-authing every fresh box would be toil, so authenticated state is reused via
snapshots, not a secrets store:
- `claudebox snapshot <n> [label]` — checkpoint after login + clone.
- `claudebox new --name <n2> --from <src>[/<snapshot>]` — clone an existing box
- `box snapshot <n> [label]` — checkpoint after login + clone.
- `box new --name <n2> --from <src>[/<snapshot>]` — clone an existing box
or snapshot (authed state and all). Isolation is preserved: the clone keeps
the `claude-dev` profile + `claudenet` + ACL.
- `claudebox restore <n> <snapshot>` — roll a box back to a checkpoint.
- `box restore <n> <snapshot>` — roll a box back to a checkpoint.
Log in once → snapshot → spin up authed boxes from it.
## The box announces itself to the agent
cloud-init installs a global `~/.claude/CLAUDE.md` in every box telling Claude it
is running in a claudebox (trust-less, ephemeral, creds-free) and to treat a
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
execution.
@ -54,13 +54,13 @@ 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
not, you set up by hand. The tool enforces no contract; there is no `install`.
## What claudebox owns, and what it doesn't
## What box owns, and what it doesn't
Boxes are ordinary Incus instances, tagged `user.claudebox=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:
> **claudebox owns a command when it must enforce an invariant Incus cannot see:**
> **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
> isolation stack (`claude-dev` profile + `claudenet` + ACL), or the creds-free
> snapshot→clone workflow. Everything else is Incus's job.
@ -76,17 +76,17 @@ The rule cuts both ways, and that's the point:
Two mechanisms keep this honest.
**The command table** (`CMDS` in `bin/claudebox`) is the single source of truth
**The command table** (`CMDS` in `bin/box`) is the single source of truth
for what exists, its synopsis, its help line, its preconditions and what runs.
Dispatch and help are both rendered from it, so the help cannot describe a
command that doesn't exist — the failure that produced #8. A thin verb is one
row; a verb that can't be expressed as a row and enforces no invariant of ours
doesn't belong in the tool.
**The escape hatch** — `claudebox incus <box> -- <args...>` — resolves and
**The escape hatch** — `box incus <box> -- <args...>` — resolves and
tag-checks the box, then hands the rest to Incus verbatim. It means "no" to a
proxy request is not "you can't do that", and it keeps the one rail that matters:
you cannot aim it at an instance claudebox didn't mint. If the command can move
you cannot aim it at an instance box didn't mint. If the command can move
the box off the isolation stack (profile, network, device, `security.*`), it
warns and proceeds — from there the trust boundary is yours to keep.
@ -102,7 +102,7 @@ That last clause is the one that was assumed and turned out to be false, so it
is spelled out here with the mechanism, and `drill/` tests it on every run.
- **Box → host, LAN, RFC1918, CGNAT, link-local:** the `claude-isolate` ACL.
- **Box → box: an nftables *bridge-family* rule** (`host/claudebox-firewall.sh`).
- **Box → box: an nftables *bridge-family* rule** (`host/box-firewall.sh`).
It cannot be an ACL rule. Two boxes on one bridge share an L2 segment, so
their frames are *switched* between bridge ports and never traverse the
netfilter path an L3 ACL lives on — the ACL looked airtight (it drops

View file

@ -1,7 +1,7 @@
# The `.claudebox/` convention
`claudebox` mints trust-less, creds-free, isolated VMs with Claude Code already
installed (`claudebox new/shell/snapshot/restore/exec/down/start/rm/status`). The
`box` mints trust-less, creds-free, isolated VMs with Claude Code already
installed (`box new/shell/snapshot/restore/exec/down/start/rm/status`). The
tool knows **nothing** about your project. There is no `install` step and no
host-run setup script.
@ -23,12 +23,12 @@ shell that the host executes.
## How it's consumed
Every box ships a global `~/.claude/CLAUDE.md` telling Claude it is inside a
claudebox and to treat a repo's `.claudebox/` folder as its bootstrap runbook.
box and to treat a repo's `.claudebox/` folder as its bootstrap runbook.
So the whole flow is:
```
claudebox new # get a box
claudebox shell # get in
box new # get a box
box shell # get in
git clone <repo> && cd <repo>
claude # Claude reads .claudebox/ and brings the project up
```

View file

@ -1,6 +1,6 @@
# The drill
An end-to-end rehearsal of claudebox against a **real** Incus: install the CLI,
An end-to-end rehearsal of box against a **real** Incus: install the CLI,
set up the host, mint boxes, drive the whole surface, check that the isolation
actually holds — and run the full
[#15 audit](https://github.com/heavy-duty/claudebox/issues/15), including a live
@ -32,7 +32,7 @@ The drill mutates the host in phase D; an aborted run can leave a network that
mints boxes with **no DNS**.
**Iterating on the drill?** Read [RUNS.md](RUNS.md) first — it is the run log:
what the audit has answered so far, the bugs the drill has found in claudebox,
what the audit has answered so far, the bugs the drill has found in box,
the traps this script has already fallen into (every one cost a run), how to
diagnose a stall, and how to run a single probe by hand instead of paying for a
whole run.
@ -46,7 +46,7 @@ wrong. The drill runs the real thing.
## What it checks
**A. Incus semantics.** The assumptions claudebox is built on, probed directly:
**A. Incus semantics.** The assumptions box is built on, probed directly:
that `incus config get <inst> user.claudebox` returns `1` (this is on the path of
*every* box command — if it lies, everything fails closed); that the
`user.claudebox=1` list filter selects our instances and excludes an untagged
@ -62,7 +62,7 @@ and its isolation warning, the `rm` confirmation guard, and the CLI contract
(typo'd command, typo'd flag, `list <box>`).
**The boundary** gets its own treatment: the drill launches an instance
claudebox did *not* mint, aims `down`, `rm` and the escape hatch at it, and
box did *not* mint, aims `down`, `rm` and the escape hatch at it, and
requires all three to refuse — and the instance to still be standing afterwards.
**C. Isolation baseline (#15 section A).** From inside a real box: public egress
@ -88,6 +88,6 @@ a **design veto** for #16, caught before the code is written.
design. The drill confirms Claude Code is installed and runnable; authenticating
is yours.
If the host has no `/dev/kvm`, claudebox falls back to container mode. The drill
If the host has no `/dev/kvm`, box falls back to container mode. The drill
still runs, but it says loudly that **the VM trust boundary was not validated**
rather than passing quietly on a weaker one.

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/claudebox/host/setup-host.sh"
inf "fix: re-run ~/.local/share/box/host/setup-host.sh"
[ "$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/claudebox/host/setup-host.sh"
inf "fix: re-run ~/.local/share/box/host/setup-host.sh"
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/claudebox/host/setup-host.sh (it pins the resolver)"
inf "fix: re-run ~/.local/share/box/host/setup-host.sh (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

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# drill.sh — end-to-end drill for box (the claudebox repo), against a real Incus.
# drill.sh — end-to-end drill for box, against a real Incus.
#
# ⚠ DESTRUCTIVE, AND MEANT TO BE. Run it on a THROWAWAY host you can format.
# It installs Incus, rewrites the host's firewall rules, installs a systemd
@ -32,8 +32,8 @@
# false FAILs on the first live run. The pipeline verdict must be grep's alone.
set -u
REPO="${CLAUDEBOX_REPO:-heavy-duty/claudebox}"
REF="${CLAUDEBOX_REF:-main}"
REPO="${BOX_REPO:-heavy-duty/claudebox}"
REF="${BOX_REF:-main}"
YES=0; KEEP=0
SELF="$(readlink -f "$0")"
@ -170,7 +170,7 @@ EOF
fi
phase "Installing box ($REPO@$REF)"
CLAUDEBOX_REPO="$REPO" CLAUDEBOX_REF="$REF" \
BOX_REPO="$REPO" BOX_REF="$REF" \
bash -c "$(curl -fsSL "https://raw.githubusercontent.com/$REPO/$REF/install.sh")" \
|| { echo "install failed"; exit 1; }
export PATH="$HOME/.local/bin:$PATH"
@ -206,10 +206,10 @@ EOF
fi
inf "running setup-host.sh (first pass: may only add you to incus-admin)…"
~/.local/share/claudebox/host/setup-host.sh || true
~/.local/share/box/host/setup-host.sh || true
# The group we were just added to isn't in this shell's credentials yet.
inf "re-entering inside the incus-admin group…"
exec sg incus-admin -c "IN_GROUP=1 CLAUDEBOX_REPO='$REPO' CLAUDEBOX_REF='$REF' KEEP=$KEEP bash '$SELF' --in-group"
exec sg incus-admin -c "IN_GROUP=1 BOX_REPO='$REPO' BOX_REF='$REF' KEEP=$KEEP bash '$SELF' --in-group"
fi
export PATH="$HOME/.local/bin:$PATH"
@ -259,7 +259,7 @@ left="$(incus list --format csv --columns n 2>/dev/null | tr '\n' ' ')"
[ -n "$left" ] && inf "instances still on this host (not ours, left alone): $left"
inf "running setup-host.sh (in-group pass: network, ACL, profile, firewall)…"
if ! timeout -k 10 300 ~/.local/share/claudebox/host/setup-host.sh; then
if ! timeout -k 10 300 ~/.local/share/box/host/setup-host.sh; then
echo "drill: setup-host.sh failed or timed out (>5 min)." >&2
echo " it should take seconds on a host that already has incus. usual causes:" >&2
echo " · instances still attached to boxnet while its ACLs are reconfigured" >&2
@ -367,7 +367,7 @@ phase "B. The box surface"
# ===========================================================================
# Compare against the installed tree's VERSION file, not a hardcoded number —
# a pinned literal here would fail the drill on every release.
expected="$(cat "$HOME/.local/share/claudebox/VERSION" 2>/dev/null || echo '?')"
expected="$(cat "$HOME/.local/share/box/VERSION" 2>/dev/null || echo '?')"
v="$(box --version 2>&1)"
case "$v" in *"$expected"*) ok "box --version → $v" ;; *) no "version mismatch: CLI says '$v', VERSION file says '$expected'" ;; esac
@ -396,7 +396,7 @@ box new --name tpl --template nosuch 2>&1 | grep -q 'no such template' \
# The one rule that keeps templates honest: no key can name a network. Plant a
# bad template in the installed tree (the drill owns this host), expect the
# parser to reject it BY NAME, remove it.
badt="$HOME/.local/share/claudebox/templates/cbdrill-bad"
badt="$HOME/.local/share/box/templates/cbdrill-bad"
mkdir -p "$badt" && printf 'BOX_IMAGE="x"\nBOX_USER="y"\nBOX_NETWORK="lan"\n' >"$badt/box.env" && : >"$badt/user-data.yaml"
box new --name tpl --template cbdrill-bad 2>&1 | grep -q "unknown key 'BOX_NETWORK'" \
&& ok "a template cannot name a network — BOX_NETWORK rejected by name" \
@ -762,7 +762,7 @@ phase "M. Migration — the pre-0.4.0 → box transition (host/migrate-host.sh)"
# tag on the OLD network — exactly what a pre-0.4.0 host carries. Then prove
# migrate-host.sh moves it onto the new stack with its identity intact, and
# retires the legacy stack only once it is empty.
MIG="$HOME/.local/share/claudebox/host/migrate-host.sh"
MIG="$HOME/.local/share/box/host/migrate-host.sh"
if [ ! -f "$MIG" ]; then
no "migrate-host.sh not installed — cannot drill the transition"
else
@ -853,5 +853,5 @@ fi
echo
inf "this host still has Incus, boxnet, the ACL, the profile and the firewall rules"
inf "(plus, unless re-run: dns.mode=none and NIC filtering from the D phase)."
inf "to undo: ~/.local/share/claudebox/host/teardown-host.sh [--purge-incus]"
inf "to undo: ~/.local/share/box/host/teardown-host.sh [--purge-incus]"
[ "$fail" -eq 0 ]

View file

@ -72,4 +72,4 @@ if $purge; then
fi
fi
echo "Teardown complete. (Your ~/.local/bin/box symlink and ~/.local/share/claudebox remain — remove by hand if wanted.)"
echo "Teardown complete. (Your ~/.local/bin/box symlink and ~/.local/share/box remain — remove by hand if wanted.)"

View file

@ -1,19 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
# claudebox installer — intended for: curl -fsSL .../install.sh | bash
# box installer — intended for: curl -fsSL .../install.sh | bash
#
# Downloads the claudebox repo tarball, installs the whole tree under
# $DEST, and puts a `box` symlink on PATH via $BINDIR.
# Downloads the box source tarball from its GitHub repo (heavy-duty/claudebox —
# the repo keeps the old name; only the tool is 'box'), installs the whole tree
# under $DEST, and puts a `box` symlink on PATH via $BINDIR.
REPO="${CLAUDEBOX_REPO:-heavy-duty/claudebox}"
REF="${CLAUDEBOX_REF:-main}"
DEST="${CLAUDEBOX_HOME:-$HOME/.local/share/claudebox}"
BINDIR="${CLAUDEBOX_BIN:-$HOME/.local/bin}"
REPO="${BOX_REPO:-heavy-duty/claudebox}"
REF="${BOX_REF:-main}"
DEST="${BOX_HOME:-$HOME/.local/share/box}"
BINDIR="${BOX_BIN:-$HOME/.local/bin}"
log() { printf 'claudebox-install: %s\n' "$*"; }
warn() { printf 'claudebox-install: WARNING: %s\n' "$*" >&2; }
die() { printf 'claudebox-install: ERROR: %s\n' "$*" >&2; exit 1; }
log() { printf 'box-install: %s\n' "$*"; }
warn() { printf 'box-install: WARNING: %s\n' "$*" >&2; }
die() { printf 'box-install: ERROR: %s\n' "$*" >&2; exit 1; }
# --- prerequisites ---------------------------------------------------------
command -v curl >/dev/null 2>&1 || die "curl is required but was not found. Please install curl and re-run."
@ -26,18 +27,19 @@ trap cleanup EXIT
URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz"
log "installing box (the claudebox repo) ($REPO@$REF)"
log "installing box from $REPO@$REF"
log "downloading $URL"
curl -fsSL "$URL" -o "$TMPDIR/claudebox.tar.gz" \
curl -fsSL "$URL" -o "$TMPDIR/box.tar.gz" \
|| die "failed to download $URL"
log "extracting archive"
tar -xzf "$TMPDIR/claudebox.tar.gz" -C "$TMPDIR" \
tar -xzf "$TMPDIR/box.tar.gz" -C "$TMPDIR" \
|| die "failed to extract archive"
# GitHub archives extract to a single top-level dir like claudebox-<ref>/
# GitHub names the archive's top dir after the REPO, which is still 'claudebox':
# it extracts to claudebox-<ref>/. That is repo-derived, not a stray brand.
EXTRACTED="$(find "$TMPDIR" -maxdepth 1 -type d -name 'claudebox-*' | head -n1)"
[ -n "$EXTRACTED" ] || die "could not find extracted claudebox-* directory in archive"
[ -n "$EXTRACTED" ] || die "could not find the extracted source directory in archive"
[ -f "$EXTRACTED/bin/box" ] || die "archive does not contain bin/box — is $REPO@$REF correct?"
# --- atomically replace $DEST ---------------------------------------------
@ -59,6 +61,13 @@ if [ -L "$BINDIR/claudebox" ]; then
rm -f "$BINDIR/claudebox"
log "removed the old claudebox symlink — the command is 'box' now (your existing boxes keep working)"
fi
# 0.5.0 moved the install tree from ~/.local/share/claudebox to ~/.local/share/box.
# Sweep the old tree so an upgrade does not leave a stale copy behind.
OLD_DEST="$HOME/.local/share/claudebox"
if [ -d "$OLD_DEST" ] && [ "$OLD_DEST" != "$DEST" ]; then
rm -rf "$OLD_DEST"
log "removed the old install tree at $OLD_DEST (it now lives at $DEST)"
fi
# --- PATH check ------------------------------------------------------------
case ":$PATH:" in

View file

@ -10,10 +10,10 @@ write_files:
permissions: '0644'
defer: true
content: |
# You are running inside a claudebox
# You are running inside a box (template: claude)
A claudebox is a trust-less, network-isolated, ephemeral VM created by the
`box` CLI (template: claude). Keep this context in mind:
A box is a trust-less, network-isolated, ephemeral VM created by the
`box` CLI. Keep this context in mind:
- **Creds-free by default.** The box starts with no Claude and no git
credentials. If you need to authenticate Claude, the operator runs `/login`
@ -49,7 +49,7 @@ runcmd:
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
- apt-get install -y nodejs
- sudo -u claude bash -lc 'curl -fsSL https://claude.ai/install.sh | bash'
# 'claudebox exec box -- claude …' runs through a NON-interactive shell, which
# 'box exec <box> -- claude …' runs through a NON-interactive shell, which
# reads no .bashrc/.zshrc — a PATH export there is invisible to it. The
# symlink makes claude reachable from every shell, interactive or not.
- ln -sf /home/claude/.local/bin/claude /usr/local/bin/claude