A bare host got two DIRTYs (the box-to-box drop 'MISSING', the host's
Tailscale resolver) and a 'NOT fit to mint (or to drill)' verdict — and
the very next drill run went 84/84 green from that exact state. Missing
from a stack and never set up are different findings: the network,
profile and ACL sections already knew this; the firewall and resolver
sections now do too. FRESH (no boxnet) downgrades both to information —
the VPN resolver is still named, as a fact about the host that setup-host
pins around, not a fault in a stack that does not exist. The clean
verdict on a fresh host now says what to actually do: run setup-host, or
the drill, which sets the host up itself.
Verified both paths live: standing stack → 'clean', post-teardown →
'fresh' with no DIRTYs.
Also: the measured drill count is 84 (README said 83 — the box-info
exposure check was a NOTE when last counted and is a PASS now).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drill section showed the commands but not the step that actually
bites: making sure the checkout you run is the code you mean to judge.
Two versions are in play — the drill script itself, and the (repo, ref)
the drill installs from GitHub and asserts before any verdict. Spell both
out, plus --repo/--ref for drilling a release or a PR branch.
Also catch drill.sh's REPO default up with the rename — it still said
heavy-duty/claudebox (GitHub redirects it, but the default should name
the repo that exists).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83 checks once the resource-flag assertions land; the '81 passing' claim
belongs to the last green run and the next one re-earns it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolution is most-specific-first: flag > BOX_* env (kept — it is the
scripting form and how the drill shrinks boxes on small hosts) > the
template's box.env > defaults. Values pass to Incus verbatim (limits.cpu,
limits.memory, root size=) — its units, its validation; box adds no
parser. Resources are all a flag can touch: there is still no flag for a
network or a security.* key, on purpose.
Flags shape a fresh mint only — --from refuses them, a clone carries its
source's resources. An explicit --disk on a container mint gets a note
instead of a silent drop (a container's root rides the pool).
The drill's blank mint now carries --cpu 1 --memory 1GiB and asserts the
limits landed — which is also the precedence proof, since the drill
exports BOX_CPU/BOX_MEMORY on small hosts — plus a negative check that
--from refuses resource flags.
Verified live (container mint, image cached): BOX_CPU=3 + --cpu 1
--memory 1GiB → limits.cpu=1, limits.memory=1GiB; --from + --cpu exits 2
before touching anything; container --disk prints the note.
Closes#57
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README told the story around Claude Code even though the tool ships
`codex` and `grok` templates and treats every coding agent the same. Reframe
the generic prose — the intro, creds-free line, `.box/` runbook, quick start,
snapshot copy, isolation boundary, recipes, and non-goals — to speak of "the
coding agent" while keeping `claude` as a named, concrete example (it's still
where the project started). No behavior or command changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
README title, install URL, and the claude-template blurb now use the
post-rename name; install.sh's REPO default follows (GitHub redirects the
pre-rename URLs, and BOX_REPO still overrides). The two survivors are
literal legacy artifact names that must keep their spelling: the pre-0.4.0
user.claudebox=1 tag and the old claudebox symlink the installer retires.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The version callout now leads with what 0.5.0 adds (codex+grok templates,
box expose, setup-host/teardown-host/migrate-host as verbs) and keeps
0.4.0's clean-cut terms beneath it. New 'See a dev server' section
documents expose's contract: loopback-only listen, 0.0.0.0 in-box,
per-port, visible in box info. Commands block synced to the actual table
(expose synopsis, host verbs, --remote gone, default template is blank —
the text said claude). Drill paragraph now names the full sweep: every
template cold, the expose door, the legacy re-home.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fetched https://x.ai/cli/install.sh and read it, rather than inferring
the layout from docs. Three facts, every one of which the template had
wrong:
· the CLI installs as 'grok' (with an 'agent' alias) — NOT 'grok-build'.
So the drill was checking a command that never existed.
· BIN_DIR defaults to $HOME/.grok/bin, and what lands there is a
SYMLINK into the versioned download dir — which is exactly why the
template's 'find -type f' found nothing.
· GROK_BIN_DIR can override the directory.
The install was almost certainly succeeding the whole time; the template
was hunting for the wrong name, as the wrong file type, in the wrong
place. Now it links the known path onto the system PATH, asserts
'grok --version' answers, and dumps what the installer actually left if
the upstream layout ever moves.
Also corrected: the drill's version check (grok, not grok-build), the
agent briefing ('grok login'), the template description, and the README
row. The lesson is the repo's oldest one — read the thing, don't reason
about it.
Two things:
1. The host scripts are first-class verbs now — 'box setup-host',
'box teardown-host [--purge-incus]', 'box migrate-host --box <n>'.
Nobody should have to run ~/.local/share/box/host/<script>.sh; that
read like an external script and exposed an install path. Each verb
execs the installed script with its flags passed through (same
pattern as 'box doctor'). README, doctor hints, and the uninstall
section point at the verbs now.
2. The repo-runbook convention is '.box/', not '.claudebox/'. Renamed
across docs and the templates' agent briefing; the briefing tells
the agent to read either, and box-recipe.md notes the rename, so
repos still shipping '.claudebox/' keep working through the
transition. (Consuming repos rename their own folder — tracked
separately.)
Also widened the help command column for the longer verb names, and
fixed one sed-casualty where a broad '.claudebox'→'.box' pass had
turned the README's legacy user.claudebox tag into user.box.
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.
The 'no inbound path' contract is one notch too absolute for the tool's
own flagship workflow: coding in a box, a dev server on :3000, and no
way to open it in your browser. expose is the deliberate un-screwing.
- Loopback only, always: the host side listens on 127.0.0.1, never
0.0.0.0 — no other machine can reach the box; only this host gets a
door. No flag widens it (that is the escape hatch's job).
- A verb, per-port, reversible, visible: each exposure is a named proxy
device (expose-<port>); --list and box info show it, --remove undoes
it. A box with a hole says so.
- Mechanism (VMs): an Incus proxy device forwards host loopback to the
box's ip:port, plus a SCOPED ingress ACL allow (this box's ip + this
port only) so the forkproxy's connection survives the default drop —
the drill decides whether that allow is needed or redundant. The
in-box server must listen on 0.0.0.0 (a VM's forwarder reaches it over
the network); inside an isolated box that is safe.
Drill phase E: start a detached listener in a box, expose it, prove the
HOST loopback reaches it, prove a NON-exposed port is still dropped (A7
survives), prove --remove shuts the door.
Closes#55
Two coding-CLI templates mirroring claude's shape: a box.env + verbatim
cloud-init, inheriting the box-net placement contract structurally, no
new design.
- codex: OpenAI Codex CLI via 'npm i -g @openai/codex' (the SCOPED
package; needs Node 22), symlinked onto the non-interactive exec PATH
via 'npm prefix -g' — the same PATH fix the claude template needed.
- grok: xAI Grok Build via the official 'curl x.ai/cli/install.sh',
run AS the grok user (the installer drops into $HOME); the binary is
found and symlinked to /usr/local/bin.
Install commands verified upstream at implementation time, per the
issue's rule (npmjs.com/package/@openai/codex, x.ai/cli). Each gets an
AGENTS.md-style context file telling the agent it lives in a
disposable, isolated, creds-free box.
Drill: templates listing now expects four; a compact per-template smoke
(mint, '<cli> --version' via box exec, remove) validates each payload
installs and lands on the exec PATH — the generic mechanic is already
proven by blank+claude and not repeated.
Closes#54
Follow-up to the rename, per operator direction — the divergence is
reversed and the cut is complete:
- Host stack: boxnet (10.88.0.0/24 — a pre-rename host may still carry
claudenet on 10.87, two bridges must not claim one subnet),
box-isolate, nft tables 'inet box'/'bridge box', box-firewall.{sh,
service}. teardown-host now strips BOTH name generations, so one
script uninstalls a host of any age.
- Default template is blank: 'box new --name x' mints bare Debian;
the claude box is '--template claude'. The login hint follows the
EFFECTIVE template read off the instance, so clones of claude boxes
still get it and blank boxes are not told to run a binary they lack.
- The drill validates templates: listing, unknown-template refusal,
the allowlist rejecting BOX_NETWORK by name, and a full blank mint —
default resolves to blank, metadata stamped, box-net placement, exec
lands in 'dev', no claude binary, and isolation parity (egress +
pinned DNS) on the same contract as every template.
- drill/wipe.sh: scorched earth for drill hosts. Both tag generations,
every drill-named instance, networks/ACLs/profiles/firewall of both
generations, cached images, and (--purge-storage) the default pool.
Ends by asserting the ABSENCE of every artifact rather than trusting
the removals' exit codes.
The tool underneath was already generic: a thin, honest wrapper over
Incus. What was Claude-specific was welded on — one image, one profile,
one cloud-init file, one hardcoded 'sudo -u claude'. The weld is now a
template.
The mechanic: 'box new' stamps the template's identity onto the
instance (user.box=1, user.box.template, user.box.user); shell/exec/
tmux read the user back off the instance, and 'incus copy' carries
user.* keys (audit B2), so a clone knows what it is without consulting
the template. Templates are box.env (parsed against a strict allowlist,
never sourced — no key for a network exists, on purpose) plus a
verbatim cloud-init. Every template launches with the shared box-net
profile: the isolated NIC and root disk, nothing template-controlled —
resources land per-instance from box.env, overridable via BOX_CPU/
BOX_MEMORY/BOX_DISK (which is also how the drill shrinks boxes on a
small host now that profile edits can't).
The three open calls, taken as recommended: clean cut at 0.4.0 (no
claudebox shim; the installer retires the old symlink); default
template = claude (muscle memory survives); repo stays heavy-duty/
claudebox, binary is box.
Compat is the tag, not the name: resolve_box and list honor the legacy
user.claudebox=1 forever, and the legacy tag maps to the claude user —
a pre-rename box lists, shells, clones, unchanged.
Deliberate divergence from #17's table: the host-stack resource names
(claudenet, claude-isolate, nft tables, claudebox-firewall.*) are NOT
renamed — they are host-internal, invisible to users, and renaming
them breaks every provisioned host for zero user-visible gain.
claude-dev is no longer created; setup-host creates box-net, teardown
removes both.
Closes#17
Every fault the drill's doctor diagnoses is a user's fault first: a
wedged Incus daemon, a dnsmasq that silently isn't serving, a VPN
resolver boxes inherit, isolation claimed by config but off in the
kernel — each has killed a cold mint or weakened a boundary, with a
cloud-init error that names none of them. The CLI half-admitted it:
cmd_new's failure path hand-pointed at issue #33, doing one special
case of a doctor's job inline.
The verb delegates to the installed drill/doctor.sh (the tree ships
whole) — one hardened script, two audiences. Its flags pass through;
the verdict now reads 'fit to mint boxes (and to drill)'; the mint-
failure hint ends with 'claudebox doctor' instead of the hand-rolled
diagnosis.
Closes#46
'shell' is a child of the exec connection: drop the terminal and a long
Claude run inside it is SIGHUP'd — which has already cost one run. tmux
is installed in every box but relying on remembering to start it is how
that run was lost.
New verb, not a changed default: 'tmux new-session -A -s <session>'
attaches when the session exists and creates it when it doesn't, so
starting work and reattaching after a disconnect are the same command.
The session name (default: main) gives parallel streams in one box.
'shell' stays bare on purpose — two verbs, two contracts — so tmux's
costs (nesting, scrollback semantics, exit-vs-detach) are only paid
when asked for.
Closes#6
The Isolation section predated the #16 arc: it described the ACL and the
host firewall, and none of what that audit shipped — port-isolated NICs
(the actual sibling boundary), dns.mode=none, the pinned resolver. It
also never mentioned that the contract is tested at all.
State the contract layer by layer as it exists on main, and add a
'Measured, not claimed' section: the drill and doctor, what each does,
which one mutates the host, and the standing score — 47/47 from a bare
host (run 13, full teardown → rebuild → drill).
Every incus verb is a candidate feature request, and wrapping them one at a
time grows a worse incus. This lands the rule instead: claudebox owns a command
when it must enforce an invariant incus cannot see — the user.claudebox=1
boundary, the isolation stack, or the creds-free snapshot workflow. Everything
else is incus's job, and now has a door.
- CMDS table: one row per command, carrying synopsis, preconditions, summary,
action and success message. Dispatch AND help are rendered from it, so the
help can no longer describe a command that doesn't exist — the drift that
produced #8 is now impossible, not merely fixed.
- rename, via a table row: it needs the box stopped (incus won't rename a
running instance), so it says so instead of leaking an incus error.
- `claudebox incus <box> -- <args...>`: the escape hatch. Box resolved and
tag-checked, rest passed to incus verbatim, {} substituted, command echoed.
Warns when it can move a box off the isolation stack.
- The boundary is now ENFORCED, not assumed: every box-taking command resolves
through the user.claudebox=1 tag, so claudebox will not stop, rename or delete
an instance it didn't mint.
- The rule, written into docs/claudebox-design.md.
Closes#11
The help was `sed -n '2,9p' "$0"` — the script scraping its own header comment
by line number — with no sections, no per-command help, no `-h`, and no
`--version`. Worse, parts of the interface it described weren't real: `--force`
was parsed and never read, so `claudebox rm work` force-deleted a running box
with no confirmation while advertising a flag that implied the bare form was
the careful one.
- usage()/help_cmd(): NAME/USAGE/COMMANDS/OPTIONS/EXAMPLES/EXIT STATUS, plus a
page per command. `claudebox help <cmd>` and `claudebox <cmd> --help`.
- `-h`/`--help` anywhere; `--version`/`-V` (VERSION file).
- Unknown command → stderr, exit 2, and a did-you-mean by edit distance.
- Unknown options are rejected, not swallowed as positionals: `snapshot work
--labl x` no longer names the snapshot "--labl". `--` still passes everything
through for exec.
- `--force` is real: `rm` confirms on a TTY, refuses without one unless forced.
BEHAVIOR CHANGE — a scripted `claudebox rm` now needs `--force`.
- Exit 2 for usage errors, 1 for runtime failures; accurate per-command usage
strings (`exec` no longer omits `-- <cmd...>`).
- shellcheck is clean: SC2034 (dead `force`) and SC2015 both gone.
Closes#8
`status` was the only lister: misnamed, silently ignoring the `<box>` its own
help advertised, and unable to show a snapshot label — the one thing the
README's snapshot→clone flow requires you to know. Typing the obvious
`claudebox list` printed the help and exited 0.
- `list` — a table of your boxes (name, state, type, snapshot count).
- `info <box>` — detail, the snapshot labels, and the `--from` line to clone one.
- `list <box>` is a wrong guess we can answer: it points at `info`.
- `--json` passthrough; `--remote` honored; a real error on an unknown box.
- `status` stays as a deprecated alias for `list` so muscle memory keeps working.
Reads are filtered by the `user.claudebox=1` tag, so an Incus instance
claudebox didn't mint is never reported on or touched.
Closes#7
The fork claude-hdb/claudebox is private, so an anonymous 'curl | bash' against
it 401s. Point the installer default and the README one-liner at the public
canonical repo so the install flow works without auth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A CLI that mints trust-less, network-isolated Incus VMs with Claude Code
installed. Boxes are strictly creds-free — the operator logs into Claude
interactively inside; authenticated state is reused via snapshots. The tool
knows nothing about projects; a repo ships an optional agent-facing .claudebox/
runbook that Claude reads.
- bin/claudebox: new/shell/exec/snapshot/restore/down/start/rm/status; creds-free
'new' (fresh launch or clone via --from <src>[/<snap>]).
- cloud-init: global ~/.claude/CLAUDE.md self-describing the box + .claudebox/ runbook.
- install.sh: curl-pipe-bash installer.
- host/: Incus isolation stack (claudenet + claude-isolate ACL + claude-dev
profile + firewall).
- docs/: design + .claudebox/ convention.
Initial canonical import (prototyped separately; re-homed onto the heavy-duty fork).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>