box/drill
claude-hdb c11f3d7552 feat!: claudebox becomes box — the Claude box is one template among several
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
2026-07-14 14:22:50 +00:00
..
doctor.sh feat!: claudebox becomes box — the Claude box is one template among several 2026-07-14 14:22:50 +00:00
drill.sh feat!: claudebox becomes box — the Claude box is one template among several 2026-07-14 14:22:50 +00:00
README.md fix(drill): stop poisoning the host, and add a doctor to prove it 2026-07-14 00:32:03 +00:00
RUNS.md docs(drill): record runs 11–13 — the contract measured at zero, from a bare host 2026-07-14 12:52:02 +00:00

The drill

An end-to-end rehearsal of claudebox 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, including a live rehearsal of the hardening #16 proposes. It ends with a block of audit answers to paste into #15.

It rearranges the host it runs on. Incus, a systemd unit, a network, an ACL, a profile, rewritten firewall rules — and, in the last phase, deliberate mutations to the network and profile. Run it on a machine you can format — a spare server, a cloud VM you'll destroy, a VM on your laptop. Not your workstation.

git clone https://github.com/heavy-duty/claudebox && cd claudebox
bash drill/drill.sh --yes      # run and forget; omit --yes to be asked first

Useful flags: --ref <branch> (drill a branch rather than main), --keep-boxes (leave the boxes up to poke at — note the last phase's network and profile mutations stay applied with them).

Exit 0 means every check passed. Roughly 20 minutes, most of it the cold box.

Something wrong with the host? bash drill/doctor.sh — it reports whether the host is fit to drill (network, profile, ACL, leftover boxes, whether a box can still resolve DNS), and --fix reverts what an aborted run left behind. 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 first — it is the run log: what the audit has answered so far, the bugs the drill has found in claudebox, 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.

Why it exists

The repo has no tests and no CI, and the CLI is a shell script that shells out to incus. That means the interesting failures are not in the bash — they are in what Incus actually does, which is exactly what unit tests would stub out and get wrong. The drill runs the real thing.

What it checks

A. Incus semantics. The assumptions claudebox 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 one; that --columns nstS gives four clean CSV fields; that the state column reads RUNNING; that incus rename really does refuse a running instance; that snapshot-list's first CSV field is the label; that an unset config key reads as empty with exit 0 (#15 B4); and that incus copy preserves user.* keys (#15 B2 — the whole template-metadata design in #17 rests on it).

B. The surface. Mint, list, info, snapshot, clone-from-a-snapshot-of-a- renamed-box, rename (running must refuse, stopped must work), the escape hatch 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 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 works; the box cannot reach a listener on the host's claudenet gateway; RFC1918 is dropped; a sibling box is unreachable (a listener runs on the peer so "refused" — the packet arrived — cannot masquerade as "dropped"); whether DNS enumerates the sibling is recorded (#12 predicts it leaks today; that is audit data, not a failure); IPv6 is off; and the host cannot connect into a box.

D. Hardening rehearsal (#15 section B). The host is disposable, so the drill applies the exact changes #16 proposes and watches what breaks: dns.mode=none (must kill sibling resolution, must not kill egress), security.mac_filtering + security.ipv4_filtering on the NIC (the box and its in-box Docker must keep working), and @internal as an ACL drop destination on a bridge network (if accepted and egress survives, #16's sibling drop is renumber-proof by construction; if not, #16 derives the subnet instead). A FAIL in this phase is a design veto for #16, caught before the code is written.

What it does not check

claude /login — it's interactive by design, and the box is creds-free by 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 still runs, but it says loudly that the VM trust boundary was not validated rather than passing quietly on a weaker one.