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
9 changed files with 75 additions and 66 deletions
Showing only changes of commit 4eb6b35a7b - Show all commits

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. nobody home — not a box with the safety off.
**The tool knows nothing about your projects.** You just `git clone` inside a **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 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. design rationale.
> **0.4.0 is a clean cut**: the CLI is `box` (no `claudebox` shim), the host > **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 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 `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.) retires the old `claudebox` symlink. (No `git clone` needed.)
## One-time host setup (Ubuntu 24.04 / Debian 13) ## One-time host setup (Ubuntu 24.04 / Debian 13)
```sh ```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 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 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 — stack (profile, network, device, `security.*`), box warns and proceeds —
the trust boundary is then yours to keep. See 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. command surface is a table.
## Isolation ## Isolation
@ -232,14 +232,14 @@ inherit.
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 `.claudebox/`
folder — a runbook Claude reads and follows (install deps, start services, folder — a runbook Claude reads and follows (install deps, start services,
template env, seed data, smoke-test). It is agent-facing documentation, not a 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 ## Uninstall
```sh ```sh
~/.local/share/claudebox/host/teardown-host.sh # boxes, network, ACL, profile, firewall ~/.local/share/box/host/teardown-host.sh # boxes, network, ACL, profile, firewall
~/.local/share/claudebox/host/teardown-host.sh --purge-incus # ...and Incus itself ~/.local/share/box/host/teardown-host.sh --purge-incus # ...and Incus itself
rm -rf ~/.local/share/claudebox ~/.local/bin/box # the CLI rm -rf ~/.local/share/box ~/.local/bin/box # the CLI
``` ```
## Non-goals ## 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. 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 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 ## 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 git credentials and **no** Claude credentials. The operator authenticates
interactively *inside* the box: 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 Re-authing every fresh box would be toil, so authenticated state is reused via
snapshots, not a secrets store: snapshots, not a secrets store:
- `claudebox snapshot <n> [label]` — checkpoint after login + clone. - `box snapshot <n> [label]` — checkpoint after login + clone.
- `claudebox new --name <n2> --from <src>[/<snapshot>]` — clone an existing box - `box new --name <n2> --from <src>[/<snapshot>]` — clone an existing box
or snapshot (authed state and all). Isolation is preserved: the clone keeps or snapshot (authed state and all). Isolation is preserved: the clone keeps
the `claude-dev` profile + `claudenet` + ACL. 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. Log in once → snapshot → spin up authed boxes from it.
## The box announces itself to the agent ## The box announces itself to the agent
cloud-init installs a global `~/.claude/CLAUDE.md` in every box telling Claude it 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 repo's `.claudebox/` folder as its bootstrap runbook. No "tell it" step, no host
execution. 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 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`. 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 Boxes are ordinary Incus instances, tagged `user.claudebox=1`. That makes every
Incus verb a candidate feature request — `rename`, `info`, `file push`, on 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: 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 > 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 > isolation stack (`claude-dev` profile + `claudenet` + ACL), or the creds-free
> snapshot→clone workflow. Everything else is Incus's job. > 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. 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. 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 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 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 row; a verb that can't be expressed as a row and enforces no invariant of ours
doesn't belong in the tool. 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 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: 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 the box off the isolation stack (profile, network, device, `security.*`), it
warns and proceeds — from there the trust boundary is yours to keep. 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. 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 → 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 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 their frames are *switched* between bridge ports and never traverse the
netfilter path an L3 ACL lives on — the ACL looked airtight (it drops netfilter path an L3 ACL lives on — the ACL looked airtight (it drops

View file

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

View file

@ -1,6 +1,6 @@
# The drill # 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 set up the host, mint boxes, drive the whole surface, check that the isolation
actually holds — and run the full actually holds — and run the full
[#15 audit](https://github.com/heavy-duty/claudebox/issues/15), including a live [#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**. mints boxes with **no DNS**.
**Iterating on the drill?** Read [RUNS.md](RUNS.md) first — it is the run log: **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 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 diagnose a stall, and how to run a single probe by hand instead of paying for a
whole run. whole run.
@ -46,7 +46,7 @@ wrong. The drill runs the real thing.
## What it checks ## 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 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 *every* box command — if it lies, everything fails closed); that the
`user.claudebox=1` list filter selects our instances and excludes an untagged `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>`). (typo'd command, typo'd flag, `list <box>`).
**The boundary** gets its own treatment: the drill launches an instance **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. 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 **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 design. The drill confirms Claude Code is installed and runnable; authenticating
is yours. 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** still runs, but it says loudly that **the VM trust boundary was not validated**
rather than passing quietly on a weaker one. 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" ok "dns.mode = none — a box cannot enumerate its siblings by name"
else else
no "dns.mode = ${dns:-<unset>} — a box can RESOLVE its siblings' names and addresses" 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"; } [ "$FIX" = 1 ] && { incus network set boxnet dns.mode=none && inf "set: dns.mode=none"; }
fi fi
inf "ipv4.address = $(incus network get boxnet ipv4.address 2>/dev/null)" 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" 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 "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 "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 fi
for k in security.mac_filtering security.ipv4_filtering; do for k in security.mac_filtering security.ipv4_filtering; do
v="$(incus profile device get "$p" eth0 "$k" 2>/dev/null)" 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" 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 "· 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 "· 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" inf " or quick-test the pin alone: bash drill/doctor.sh --pin-dns"
else else
inf "boxes inherit the host's resolver (unpinned — setup-host.sh pins this now; re-run it)" 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 #!/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. # ⚠ 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 # 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. # false FAILs on the first live run. The pipeline verdict must be grep's alone.
set -u set -u
REPO="${CLAUDEBOX_REPO:-heavy-duty/claudebox}" REPO="${BOX_REPO:-heavy-duty/claudebox}"
REF="${CLAUDEBOX_REF:-main}" REF="${BOX_REF:-main}"
YES=0; KEEP=0 YES=0; KEEP=0
SELF="$(readlink -f "$0")" SELF="$(readlink -f "$0")"
@ -170,7 +170,7 @@ EOF
fi fi
phase "Installing box ($REPO@$REF)" 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")" \ bash -c "$(curl -fsSL "https://raw.githubusercontent.com/$REPO/$REF/install.sh")" \
|| { echo "install failed"; exit 1; } || { echo "install failed"; exit 1; }
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
@ -206,10 +206,10 @@ EOF
fi fi
inf "running setup-host.sh (first pass: may only add you to incus-admin)…" 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. # The group we were just added to isn't in this shell's credentials yet.
inf "re-entering inside the incus-admin group…" 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 fi
export PATH="$HOME/.local/bin:$PATH" 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" [ -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)…" 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 "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 " 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 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 — # Compare against the installed tree's VERSION file, not a hardcoded number —
# a pinned literal here would fail the drill on every release. # 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)" v="$(box --version 2>&1)"
case "$v" in *"$expected"*) ok "box --version → $v" ;; *) no "version mismatch: CLI says '$v', VERSION file says '$expected'" ;; esac 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 # 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 # bad template in the installed tree (the drill owns this host), expect the
# parser to reject it BY NAME, remove it. # 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" 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'" \ 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" \ && 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 # 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 # migrate-host.sh moves it onto the new stack with its identity intact, and
# retires the legacy stack only once it is empty. # 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 if [ ! -f "$MIG" ]; then
no "migrate-host.sh not installed — cannot drill the transition" no "migrate-host.sh not installed — cannot drill the transition"
else else
@ -853,5 +853,5 @@ fi
echo echo
inf "this host still has Incus, boxnet, the ACL, the profile and the firewall rules" 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 "(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 ] [ "$fail" -eq 0 ]

View file

@ -72,4 +72,4 @@ if $purge; then
fi fi
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 #!/usr/bin/env bash
set -euo pipefail 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 # Downloads the box source tarball from its GitHub repo (heavy-duty/claudebox —
# $DEST, and puts a `box` symlink on PATH via $BINDIR. # 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}" REPO="${BOX_REPO:-heavy-duty/claudebox}"
REF="${CLAUDEBOX_REF:-main}" REF="${BOX_REF:-main}"
DEST="${CLAUDEBOX_HOME:-$HOME/.local/share/claudebox}" DEST="${BOX_HOME:-$HOME/.local/share/box}"
BINDIR="${CLAUDEBOX_BIN:-$HOME/.local/bin}" BINDIR="${BOX_BIN:-$HOME/.local/bin}"
log() { printf 'claudebox-install: %s\n' "$*"; } log() { printf 'box-install: %s\n' "$*"; }
warn() { printf 'claudebox-install: WARNING: %s\n' "$*" >&2; } warn() { printf 'box-install: WARNING: %s\n' "$*" >&2; }
die() { printf 'claudebox-install: ERROR: %s\n' "$*" >&2; exit 1; } die() { printf 'box-install: ERROR: %s\n' "$*" >&2; exit 1; }
# --- prerequisites --------------------------------------------------------- # --- prerequisites ---------------------------------------------------------
command -v curl >/dev/null 2>&1 || die "curl is required but was not found. Please install curl and re-run." 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" 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" log "downloading $URL"
curl -fsSL "$URL" -o "$TMPDIR/claudebox.tar.gz" \ curl -fsSL "$URL" -o "$TMPDIR/box.tar.gz" \
|| die "failed to download $URL" || die "failed to download $URL"
log "extracting archive" log "extracting archive"
tar -xzf "$TMPDIR/claudebox.tar.gz" -C "$TMPDIR" \ tar -xzf "$TMPDIR/box.tar.gz" -C "$TMPDIR" \
|| die "failed to extract archive" || 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)" 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?" [ -f "$EXTRACTED/bin/box" ] || die "archive does not contain bin/box — is $REPO@$REF correct?"
# --- atomically replace $DEST --------------------------------------------- # --- atomically replace $DEST ---------------------------------------------
@ -59,6 +61,13 @@ if [ -L "$BINDIR/claudebox" ]; then
rm -f "$BINDIR/claudebox" rm -f "$BINDIR/claudebox"
log "removed the old claudebox symlink — the command is 'box' now (your existing boxes keep working)" log "removed the old claudebox symlink — the command is 'box' now (your existing boxes keep working)"
fi 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 ------------------------------------------------------------ # --- PATH check ------------------------------------------------------------
case ":$PATH:" in case ":$PATH:" in

View file

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