2026-07-14 23:59:34 +00:00
|
|
|
# box
|
2026-07-10 14:53:40 +00:00
|
|
|
|
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
|
|
|
**Headless, trust-less, throwaway dev VMs.** One command mints a fresh,
|
2026-07-15 00:15:45 +00:00
|
|
|
network-isolated Incus box from a **template**; the coding-agent templates
|
|
|
|
|
ship a CLI agent on Debian 13 — `claude` (Claude Code), `codex` (OpenAI
|
|
|
|
|
Codex), `grok` (xAI Grok). The box is the product — you log in and work;
|
|
|
|
|
destroying it loses nothing you didn't push.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
**Strictly creds-free.** A box ships with everything installed and **no**
|
2026-07-15 00:15:45 +00:00
|
|
|
credentials — no agent token, no git PAT, nothing. You authenticate
|
2026-07-10 15:00:36 +00:00
|
|
|
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.
|
|
|
|
|
|
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
|
|
|
**Templates set what's in the box, never what it can reach.** A template is
|
|
|
|
|
image + user + resources + cloud-init; the network and every security flag
|
|
|
|
|
live in a shared profile no template can touch, so `blank` is a box with
|
|
|
|
|
nobody home — not a box with the safety off.
|
|
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
**The tool knows nothing about your projects.** You just `git clone` inside a
|
2026-07-14 18:01:34 +00:00
|
|
|
box. A repo can ship an optional [`.box/`](docs/box-recipe.md)
|
2026-07-15 00:15:45 +00:00
|
|
|
runbook that the box's coding agent reads and acts on — there is no `install`
|
|
|
|
|
step and no host-run setup. See [docs/box-design.md](docs/box-design.md) for the
|
2026-07-10 15:00:36 +00:00
|
|
|
design rationale.
|
|
|
|
|
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
> **0.5.0**: two new templates (`codex`, `grok`), `box expose` — a
|
|
|
|
|
> loopback-only door to a box port, for seeing a dev server — and the host
|
|
|
|
|
> lifecycle as first-class verbs: `box setup-host`, `box teardown-host`, and
|
|
|
|
|
> `box migrate-host`, which re-homes pre-0.4.0 boxes onto the current stack
|
|
|
|
|
> and retires the legacy bridge.
|
|
|
|
|
>
|
2026-07-14 23:59:34 +00:00
|
|
|
> **0.4.0's clean cut stands**: the CLI is `box` (no legacy shim), the
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
> host stack is `boxnet`/`box-isolate`/`box-firewall` on 10.88.0.0/24, and
|
|
|
|
|
> the default template is `blank`. Boxes minted by any earlier version keep
|
|
|
|
|
> working under every verb — their legacy tag is honored forever.
|
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
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
## Install
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-07-14 23:59:34 +00:00
|
|
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/box/main/install.sh | bash
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
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.
2026-07-14 17:44:24 +00:00
|
|
|
Installs the tree to `~/.local/share/box` and links `box` onto your
|
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
|
|
|
`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.)
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
## One-time host setup (Ubuntu 24.04 / Debian 13)
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-07-14 18:01:34 +00:00
|
|
|
box setup-host # run twice if it adds you to incus-admin (re-login between)
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
Idempotent. Installs Incus and creates the isolation stack: the `boxnet` NAT
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
bridge (sibling-name resolution off, resolver pinned to public upstreams —
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
`BOX_DNS` overrides), the `box-isolate` ACL (drops all RFC1918/CGNAT/
|
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
|
|
|
link-local egress), the `box-net` profile (port-isolated NICs — boxes can't
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
reach each other), and firewall rules blocking instance → host. All rules
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
re-apply at boot via `box-firewall.service` — no post-reboot ritual. If
|
2026-07-10 15:00:36 +00:00
|
|
|
the host lacks `dnsmasq-base` (Debian cloud images skip Recommends):
|
|
|
|
|
`sudo apt-get install -y dnsmasq-base`.
|
|
|
|
|
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
A host still carrying the pre-0.4.0 stack: `box migrate-host --all-boxes`
|
|
|
|
|
re-homes each legacy box onto `boxnet` (authed state preserved), and
|
|
|
|
|
`box migrate-host --retire-legacy` removes the old bridge and profile once no
|
|
|
|
|
legacy box remains.
|
|
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
## Quick start
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-07-15 00:15:45 +00:00
|
|
|
box new --name work --template claude # a creds-free coding-agent box (~10 min cold)
|
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
|
|
|
box shell work # enter as the template's user
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
2026-07-15 00:15:45 +00:00
|
|
|
Pick whichever coding-agent template you like — `claude`, `codex`, `grok` — or
|
|
|
|
|
`blank` for none. Inside the box, authenticate as needed. The `claude` template
|
|
|
|
|
looks like this; the others follow the same shape with their own login step:
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
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
|
2026-07-15 00:15:45 +00:00
|
|
|
claude # if the repo has .box/, the agent reads it and sets up
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
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
|
|
|
## Templates
|
|
|
|
|
|
2026-07-15 00:15:45 +00:00
|
|
|
No coding agent is special — each is one template among several, and adding
|
|
|
|
|
another is just another directory. What ships today:
|
feat: codex and grok templates — the mechanic's second and third tenants
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
2026-07-14 15:59:39 +00:00
|
|
|
|
|
|
|
|
| Template | What's in it |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| `blank` | Bare Debian 13 — same isolation, no tooling. The default. |
|
2026-07-14 23:59:34 +00:00
|
|
|
| `claude` | Claude Code, creds-free — where this project started |
|
feat: codex and grok templates — the mechanic's second and third tenants
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
2026-07-14 15:59:39 +00:00
|
|
|
| `codex` | OpenAI Codex CLI, creds-free |
|
fix(grok): read the actual installer instead of guessing — the binary is 'grok'
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.
2026-07-14 20:04:10 +00:00
|
|
|
| `grok` | xAI Grok CLI, creds-free |
|
feat: codex and grok templates — the mechanic's second and third tenants
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
2026-07-14 15:59:39 +00:00
|
|
|
|
|
|
|
|
A template is a directory under `templates/`: a `box.env` (image, user,
|
|
|
|
|
resources — parsed against a strict allowlist, never sourced) and a
|
|
|
|
|
`user-data.yaml` (cloud-init, passed to Incus verbatim). The coding-CLI
|
|
|
|
|
templates are all the same shape — install the CLI, put it on PATH, drop an
|
|
|
|
|
agent-context file; none of them carry credentials.
|
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
|
|
|
|
|
|
|
|
```sh
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
box templates # list what this install can mint
|
|
|
|
|
box new --name scratch # the DEFAULT template is blank: bare Debian,
|
|
|
|
|
# same isolation, nobody home
|
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
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A template **cannot** name a network, a profile, or a `security.*` flag —
|
|
|
|
|
there is no key for them. Every box launches with the shared `box-net`
|
|
|
|
|
profile (the isolated NIC + root disk), so every template gets the identical
|
feat(new): inline resource overrides — --cpu, --memory, --disk (#57)
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>
2026-07-15 00:17:33 +00:00
|
|
|
trust boundary. Resources come from the template's `box.env`, overridable at
|
|
|
|
|
mint time — inline (`--cpu 2 --memory 3GiB --disk 20GiB`) or via
|
|
|
|
|
`BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment variables (the scripting
|
|
|
|
|
form; flags win). The template's identity (name, user) is stamped onto the instance,
|
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
|
|
|
so `shell`, `exec` and `tmux` land in the right user — and a clone still
|
|
|
|
|
knows, because `incus copy` carries the metadata.
|
|
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
## 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:
|
|
|
|
|
|
|
|
|
|
```sh
|
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
|
|
|
box snapshot work authed # checkpoint after you've logged in
|
|
|
|
|
box new --name feature --from work/authed # clone the authed state into a new box
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
2026-07-15 00:15:45 +00:00
|
|
|
`--from` copies the whole box (agent login, git creds, clones and all) while
|
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
|
|
|
preserving isolation. You can also `box new --name x --from work` to clone
|
|
|
|
|
a box's live state, or roll a box back with `box restore work authed`.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
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
|
|
|
Forgotten what you called a checkpoint? `box info work` prints the box's
|
2026-07-13 20:26:44 +00:00
|
|
|
snapshot labels and the `--from` line to clone one.
|
|
|
|
|
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
## See a dev server: `box expose`
|
|
|
|
|
|
|
|
|
|
The isolation contract says no inbound path exists — which is one "no" too
|
|
|
|
|
many when you're coding in a box and want its dev server in your browser.
|
|
|
|
|
`box expose` is the deliberate exception:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
box expose work 3000 # http://127.0.0.1:3000 → work:3000
|
|
|
|
|
box expose work 3000 8080 # or pick the host port: 127.0.0.1:8080 → work:3000
|
|
|
|
|
box expose work --list # what doors are open
|
|
|
|
|
box expose work --remove 3000 # close one
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The listen side is **always the host's own loopback** — never the network, no
|
|
|
|
|
flag to widen it — so no other machine gains a path to the box. The in-box
|
|
|
|
|
server must listen on `0.0.0.0`, not its own loopback (safe inside the
|
|
|
|
|
isolation stack: only this door can reach it). A box with a hole says so:
|
|
|
|
|
`box info` lists open exposures. Everything else on the box stays dropped —
|
|
|
|
|
the door is per-port, punched and removable at runtime.
|
|
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
## Commands
|
|
|
|
|
|
|
|
|
|
```
|
feat(new): inline resource overrides — --cpu, --memory, --disk (#57)
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>
2026-07-15 00:17:33 +00:00
|
|
|
box new --name <box> [--template <t>] [--from <src>[/<snap>]] [--cpu <n>] [--memory <size>] [--disk <size>] [--vm|--container]
|
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
|
|
|
box templates # list the templates this install can mint
|
|
|
|
|
box list # list your boxes
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
box info <box> # one box: state, IP, exposures, snapshot labels
|
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
|
|
|
box shell <box> # enter as the template's user
|
|
|
|
|
box exec <box> -- <cmd...> # run a command in the box
|
|
|
|
|
box tmux <box> [session] # attach/create a tmux session — survives disconnects
|
|
|
|
|
box snapshot <box> [label] # checkpoint (label defaults to manual-<epoch>)
|
|
|
|
|
box restore <box> <snap> # roll back to a snapshot
|
|
|
|
|
box rename <box> <new> # rename a box (stop it first)
|
|
|
|
|
box down <box> # stop (state kept; `start` resumes)
|
|
|
|
|
box start <box> # start a stopped box
|
|
|
|
|
box rm <box> [--force] # delete the box + its snapshots (asks first)
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
box expose <box> <port> [<host-port>] | --list | --remove <port>
|
|
|
|
|
# forward a box port to host loopback — see a dev server
|
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
|
|
|
box incus <box> -- <args...> # escape hatch: any incus command, box resolved
|
|
|
|
|
box doctor [--fix|--pin-dns] # is this host fit to mint boxes? diagnose from ground truth
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
box setup-host # one-time host setup: Incus, the boxnet stack, the firewall
|
|
|
|
|
box teardown-host [--purge-incus] # remove the host stack (both name generations)
|
|
|
|
|
box migrate-host --box <n> | --all-boxes | --retire-legacy
|
|
|
|
|
# move a pre-0.4.0 host onto the box stack
|
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
|
|
|
box status # deprecated alias for `list`
|
|
|
|
|
box help [<command>] # full help, or one command's page
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
fix: standard help, honest flags, and an `rm` that asks first
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
2026-07-13 20:31:01 +00:00
|
|
|
Every command takes `--help`, and options come after the command
|
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
|
|
|
(`box list --json`). Exit status: `0` ok, `1` it went wrong, `2` you asked
|
fix: standard help, honest flags, and an `rm` that asks first
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
2026-07-13 20:31:01 +00:00
|
|
|
wrong.
|
|
|
|
|
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
`new` fresh-launches from a template (default: `blank`), or with `--from`
|
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
|
|
|
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.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
feat: make the command surface a table, add rename and an escape hatch
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
2026-07-13 20:49:46 +00:00
|
|
|
## Boxes are just Incus instances
|
|
|
|
|
|
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
|
|
|
A box is an ordinary Incus instance tagged `user.box=1` (pre-0.4.0 boxes
|
2026-07-14 18:01:34 +00:00
|
|
|
carry `user.claudebox=1`, honored forever). box wraps the box lifecycle and
|
|
|
|
|
the isolation model — not all of Incus. It owns a command
|
feat: make the command surface a table, add rename and an escape hatch
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
2026-07-13 20:49:46 +00:00
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```sh
|
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
|
|
|
box incus work -- config show # instance name appended
|
|
|
|
|
box incus work -- file push x.tar {}/tmp/ # or placed with {}
|
feat: make the command surface a table, add rename and an escape hatch
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
2026-07-13 20:49:46 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
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
|
|
|
stack (profile, network, device, `security.*`), box warns and proceeds —
|
feat: make the command surface a table, add rename and an escape hatch
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
2026-07-13 20:49:46 +00:00
|
|
|
the trust boundary is then yours to keep. See
|
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.
2026-07-14 17:44:24 +00:00
|
|
|
[docs/box-design.md](docs/box-design.md) for the rule and why the
|
feat: make the command surface a table, add rename and an escape hatch
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
2026-07-13 20:49:46 +00:00
|
|
|
command surface is a table.
|
|
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
## Isolation
|
|
|
|
|
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
The contract: **a box reaches the public internet and nothing else.** Not the
|
|
|
|
|
host, not your LAN, not another box, not even another box's *name*. What
|
|
|
|
|
enforces it, layer by layer:
|
|
|
|
|
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
- **Dedicated NAT bridge** `boxnet`, IPv6 off. Every rule below is
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
IPv4-only, so IPv6 would be an uncovered path — off is part of the
|
|
|
|
|
contract, not a default.
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
- **`box-isolate` ACL** — drops all egress to private space (RFC1918,
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
CGNAT, link-local), with a single carve-out to the gateway so DNS works.
|
|
|
|
|
- **Sibling isolation, at L2** — two boxes on one bridge are *switched*,
|
|
|
|
|
never routed, so no L3 rule can separate them (learned the hard way; see
|
|
|
|
|
below). `security.port_isolation` on every box NIC plus an nft
|
|
|
|
|
bridge-family drop mean box A cannot exchange frames with box B at all.
|
|
|
|
|
- **No name-level reconnaissance** — `dns.mode=none` stops the gateway
|
|
|
|
|
resolving sibling names, and the bridge's resolver is pinned to public
|
|
|
|
|
upstreams (`no-resolv`), so tailnet names and split-DNS zones from a
|
|
|
|
|
host-level VPN don't resolve inside a box either.
|
|
|
|
|
- **Host firewall** — instance → host is dropped except DNS/DHCP, including
|
|
|
|
|
the host's public IPs. Entry is `incus exec` over the local socket only —
|
feat: 'box expose <box> <port>' — a deliberate, loopback-only door to a dev server
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
2026-07-14 16:07:17 +00:00
|
|
|
**no inbound path exists** — unless you punch one with `box expose`, and
|
|
|
|
|
that door only ever opens onto the host's own loopback (`127.0.0.1`), never
|
|
|
|
|
the network.
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
|
2026-07-15 00:15:45 +00:00
|
|
|
The VM is the trust boundary: whatever runs inside — the coding agent, or
|
|
|
|
|
anything a template ships — can run arbitrary code and touch nothing you care
|
|
|
|
|
about.
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
|
|
|
|
|
### Measured, not claimed
|
|
|
|
|
|
|
|
|
|
Every clause above is probed live by an end-to-end drill, because the one time
|
|
|
|
|
this contract was reasoned about instead of measured, the reasoning was wrong:
|
|
|
|
|
box→box traffic was "covered" by an L3 drop that L2-switched frames never
|
|
|
|
|
meet — a hole found by probing, not by reading the rules. On a bare host the
|
docs(readme): 0.5.0 — expose, codex/grok, host verbs, drill at 81/81
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>
2026-07-14 23:55:07 +00:00
|
|
|
drill installs the whole stack, mints every template cold, snapshots and
|
|
|
|
|
clones, probes every boundary from inside the boxes, opens and shuts the
|
|
|
|
|
`expose` door (and checks the contract survives it), re-homes a faithful
|
|
|
|
|
pre-0.4.0 box through `migrate-host`, and removes what it minted —
|
2026-07-15 00:21:44 +00:00
|
|
|
currently **83 checks**. [drill/RUNS.md](drill/RUNS.md) is the full
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
history, including every trap that fooled a run into a wrong verdict.
|
|
|
|
|
|
2026-07-15 00:28:15 +00:00
|
|
|
### Run the drill yourself
|
|
|
|
|
|
|
|
|
|
The drill ships in the repo, not the installed tree — run it from a checkout.
|
|
|
|
|
Two versions are in play and both must be current: **the drill script you
|
|
|
|
|
run** (a stale checkout judges the past), and **the code under test** — the
|
|
|
|
|
drill does not test your working tree; it installs box from GitHub
|
|
|
|
|
(default: `heavy-duty/box@main`) and asserts the installed tree is exactly
|
|
|
|
|
the ref it asked for before issuing any verdict.
|
|
|
|
|
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
```sh
|
2026-07-15 00:28:15 +00:00
|
|
|
git clone https://github.com/heavy-duty/box && cd box # or refresh an existing
|
|
|
|
|
git log --oneline -1 # checkout — this commit is
|
|
|
|
|
# the drill that will judge
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
bash drill/doctor.sh # read-only: is this host healthy and the stack live?
|
|
|
|
|
bash drill/drill.sh # FULL end-to-end — mutates the host; use a machine you own
|
feat!: rename the host stack too, default to blank, drill the templates, add wipe
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.
2026-07-14 14:36:39 +00:00
|
|
|
bash drill/wipe.sh # scorched earth: strip BOTH name generations, images and
|
|
|
|
|
# (--purge-storage) the pool, so a run starts from bare
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
```
|
|
|
|
|
|
2026-07-15 00:28:15 +00:00
|
|
|
To drill something other than latest `main` — a release ref, or a PR branch
|
|
|
|
|
on a fork:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
bash drill/drill.sh --ref <branch-or-tag>
|
|
|
|
|
bash drill/drill.sh --repo <owner>/<repo> --ref <branch> # a PR under review
|
|
|
|
|
```
|
|
|
|
|
|
docs(readme): the isolation contract as it ships — and measured, not claimed
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).
2026-07-14 12:47:09 +00:00
|
|
|
The doctor reads ground truth, not config claims — the kernel's `isolated on`
|
|
|
|
|
flag per bridge port, the process table, the resolver actually in use — and
|
|
|
|
|
diagnoses the host faults that have actually happened: a wedged Incus daemon,
|
|
|
|
|
a dnsmasq that silently isn't serving, a VPN resolver that boxes would
|
|
|
|
|
inherit.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
2026-07-14 18:01:34 +00:00
|
|
|
## Recipes: the `.box/` convention
|
2026-07-10 15:00:36 +00:00
|
|
|
|
2026-07-14 18:01:34 +00:00
|
|
|
A repo that wants to be easy to stand up in a box ships an optional `.box/`
|
2026-07-15 00:15:45 +00:00
|
|
|
folder — a runbook the box's coding agent 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/box-recipe.md](docs/box-recipe.md).
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
## Uninstall
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-07-14 18:01:34 +00:00
|
|
|
box teardown-host # boxes, network, ACL, profile, firewall
|
|
|
|
|
box teardown-host --purge-incus # ...and Incus itself
|
|
|
|
|
rm -rf ~/.local/share/box ~/.local/bin/box # the CLI itself
|
2026-07-10 15:00:36 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Non-goals
|
|
|
|
|
|
|
|
|
|
- **No unattended/CI bring-up.** The flow is interactive (log in, clone, ask
|
2026-07-15 00:15:45 +00:00
|
|
|
the agent). Reproducible-by-construction provisioning is out of scope.
|
2026-07-10 15:00:36 +00:00
|
|
|
- **No credential storage or injection by the tool.** Boxes are creds-free;
|
|
|
|
|
snapshots are the reuse mechanism, not a secrets store.
|