The reset truncated /etc/machine-id and called 'incus restart'. But
systemd needs a VALID machine-id to shut down cleanly — so the graceful
stop hung, incus timed out ("Failed shutting down instance, status is
Running: context deadline exceeded"), and the reboot never happened. The
clone kept its source's machine-id, hence its DUID, hence its DHCP
lease: two boxes on one address, which is the exact bug the reset exists
to prevent. Worse, the duplicate address then broke the box's networking
outright ("box cannot reach the internet"), and poisoned the isolation
run downstream.
Write a fresh VALID id with systemd-machine-id-setup instead of emptying
the file — in a VM it derives from the DMI product UUID, which Incus
makes unique per instance. Then restart with a real timeout and a forced
fallback: a clone that keeps its source's lease is worse than an unclean
stop of a box that booted thirty seconds ago.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|---|---|---|
| bin | ||
| cloud-init | ||
| docs | ||
| drill | ||
| host | ||
| profiles | ||
| .gitignore | ||
| install.sh | ||
| LICENSE | ||
| README.md | ||
| VERSION | ||
claudebox
A CLI to run headless, trust-less Claude Code in throwaway VMs. One command mints a fresh, network-isolated Incus box with Claude Code installed. The box is the product — you log in and work; destroying it loses nothing you didn't push.
Strictly creds-free. A box ships with everything installed and no credentials — no Claude token, no git PAT, nothing. You authenticate interactively inside the box. The tool never stores or injects a secret. That means there's nothing shared or committed, so it's safe for multiple operators out of the box.
The tool knows nothing about your projects. You just git clone inside a
box. A repo can ship an optional .claudebox/
runbook that Claude Code reads and acts on — there is no install step and no
host-run setup. See docs/claudebox-design.md for the
design rationale.
Install
curl -fsSL https://raw.githubusercontent.com/heavy-duty/claudebox/main/install.sh | bash
Installs the tree to ~/.local/share/claudebox and links claudebox onto your
PATH. Re-run any time to upgrade. (No git clone needed.)
One-time host setup (Ubuntu 24.04 / Debian 13)
~/.local/share/claudebox/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 claudenet NAT
bridge, the claude-isolate ACL (drops all RFC1918/CGNAT/link-local egress),
the claude-dev profile, and firewall rules blocking instance → host. All rules
re-apply at boot via claudebox-firewall.service — no post-reboot ritual. If
the host lacks dnsmasq-base (Debian cloud images skip Recommends):
sudo apt-get install -y dnsmasq-base.
Quick start
claudebox new --name work # mint a fresh, creds-free box (~10 min cold)
claudebox shell work # enter as the claude user
Inside the box, authenticate as needed:
claude # then run /login — copy the URL (press c), open it
# 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
Log in once, reuse via snapshots
Because every fresh box is creds-free, re-authenticating each time would be toil. Snapshot an authenticated box and clone from it instead:
claudebox snapshot work authed # checkpoint after you've logged in
claudebox new --name feature --from work/authed # clone the authed state into a new box
--from copies the whole box (Claude login, git creds, clones and all) while
preserving isolation. You can also claudebox new --name x --from work to clone
a box's live state, or roll a box back with claudebox restore work authed.
Forgotten what you called a checkpoint? claudebox info work prints the box's
snapshot labels and the --from line to clone one.
Commands
claudebox new --name <box> [--from <src>[/<snap>]] [--vm|--container] [--remote r]
claudebox list # list your boxes
claudebox info <box> # one box: state, IP, snapshot labels
claudebox shell <box> # enter as the claude user
claudebox exec <box> -- <cmd...> # run a command in the box
claudebox snapshot <box> [label] # checkpoint (label defaults to manual-<epoch>)
claudebox restore <box> <snap> # roll back to a snapshot
claudebox rename <box> <new> # rename a box (stop it first)
claudebox down <box> # stop (state kept; `start` resumes)
claudebox start <box> # start a stopped box
claudebox rm <box> [--force] # delete the box + its snapshots (asks first)
claudebox incus <box> -- <args...> # escape hatch: any incus command, box resolved
claudebox status # deprecated alias for `list`
claudebox help [<command>] # full help, or one command's page
Every command takes --help, and options come after the command
(claudebox list --json). Exit status: 0 ok, 1 it went wrong, 2 you asked
wrong.
new fresh-launches from cloud-init, or with --from clones an existing box or
snapshot. VM mode (--vm, the default where /dev/kvm exists) is the trust-less
target; container mode (auto-fallback, security.nesting=true) is for hosts
without nested virt — weaker isolation, dev/test only.
Boxes are just Incus instances
A box is an ordinary Incus instance tagged user.claudebox=1. claudebox 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:
claudebox incus work -- config show # instance name appended
claudebox incus work -- file push x.tar {}/tmp/ # or placed with {}
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.*), claudebox warns and proceeds —
the trust boundary is then yours to keep. See
docs/claudebox-design.md for the rule and why the
command surface is a table.
Isolation
Dedicated NAT bridge claudenet + Incus claude-isolate ACL dropping all
private-range egress, plus host-firewall rules that block instance → host
(including the host's public IPs). The box reaches the public internet and
nothing else. Entry is incus exec over the local socket only — no inbound
path exists. The VM is the trust boundary: Claude can run arbitrary code inside
and touch nothing you care about.
Recipes: the .claudebox/ convention
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.
Uninstall
~/.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/claudebox # the CLI
Non-goals
- No unattended/CI bring-up. The flow is interactive (log in, clone, ask Claude). Reproducible-by-construction provisioning is out of scope.
- No credential storage or injection by the tool. Boxes are creds-free; snapshots are the reuse mechanism, not a secrets store.