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
|
|
|
|
# box design
|
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
|
|
|
|
`box` is a CLI that mints and manages **trust-less, network-isolated VMs
|
docs: extend the agent-agnostic reframe to design, recipe, and drill docs
Same treatment as the README, applied to the prose that stood in "Claude" for
"the coding agent": box-recipe.md and box-design.md now describe the `.box/`
runbook and creds-free flow around whichever agent the box was minted with, and
name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
`~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's
"does not check" note says the drill confirms each template's CLI, not just
Claude Code. `claude` stays as the concrete login example throughout.
Left untouched (out of scope, literal identifiers): the legacy isolation-stack
names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template
files themselves, and drill/RUNS.md history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:21:22 +00:00
|
|
|
|
with a coding agent installed** (`claude`, `codex`, `grok`, or `blank` for
|
|
|
|
|
|
none). It is infrastructure, not a project provisioner.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
|
|
See issue #3 for the full reframe and rationale. This doc captures the durable
|
|
|
|
|
|
design decisions.
|
|
|
|
|
|
|
|
|
|
|
|
## Principle: separate the tool from the agent
|
|
|
|
|
|
|
docs: extend the agent-agnostic reframe to design, recipe, and drill docs
Same treatment as the README, applied to the prose that stood in "Claude" for
"the coding agent": box-recipe.md and box-design.md now describe the `.box/`
runbook and creds-free flow around whichever agent the box was minted with, and
name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
`~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's
"does not check" note says the drill confirms each template's CLI, not just
Claude Code. `claude` stays as the concrete login example throughout.
Left untouched (out of scope, literal identifiers): the legacy isolation-stack
names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template
files themselves, and drill/RUNS.md history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:21:22 +00:00
|
|
|
|
- **The tool** mints isolated boxes with the agent installed but **unauthenticated**.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
It knows nothing about projects, secrets, recipes, or memory.
|
docs: extend the agent-agnostic reframe to design, recipe, and drill docs
Same treatment as the README, applied to the prose that stood in "Claude" for
"the coding agent": box-recipe.md and box-design.md now describe the `.box/`
runbook and creds-free flow around whichever agent the box was minted with, and
name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
`~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's
"does not check" note says the drill confirms each template's CLI, not just
Claude Code. `claude` stays as the concrete login example throughout.
Left untouched (out of scope, literal identifiers): the legacy isolation-stack
names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template
files themselves, and drill/RUNS.md history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:21:22 +00:00
|
|
|
|
- **The agent** (Claude Code, Codex, Grok — whichever template, inside the box)
|
|
|
|
|
|
reads an optional `.box/` runbook in a cloned repo and acts on it. The recipe's
|
|
|
|
|
|
consumer is the reasoning agent, not host machinery.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
|
|
## Boxes are strictly creds-free
|
|
|
|
|
|
|
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
|
|
|
|
`box new --name <n>` launches a blank box: everything installed, **no**
|
docs: extend the agent-agnostic reframe to design, recipe, and drill docs
Same treatment as the README, applied to the prose that stood in "Claude" for
"the coding agent": box-recipe.md and box-design.md now describe the `.box/`
runbook and creds-free flow around whichever agent the box was minted with, and
name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
`~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's
"does not check" note says the drill confirms each template's CLI, not just
Claude Code. `claude` stays as the concrete login example throughout.
Left untouched (out of scope, literal identifiers): the legacy isolation-stack
names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template
files themselves, and drill/RUNS.md history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:21:22 +00:00
|
|
|
|
git credentials and **no** agent credentials. The operator authenticates
|
2026-07-10 15:00:36 +00:00
|
|
|
|
interactively *inside* the box:
|
|
|
|
|
|
|
docs: extend the agent-agnostic reframe to design, recipe, and drill docs
Same treatment as the README, applied to the prose that stood in "Claude" for
"the coding agent": box-recipe.md and box-design.md now describe the `.box/`
runbook and creds-free flow around whichever agent the box was minted with, and
name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
`~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's
"does not check" note says the drill confirms each template's CLI, not just
Claude Code. `claude` stays as the concrete login example throughout.
Left untouched (out of scope, literal identifiers): the legacy isolation-stack
names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template
files themselves, and drill/RUNS.md history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:21:22 +00:00
|
|
|
|
- **The coding agent** — e.g. `claude` → `/login` (paste-a-code OAuth: copy the
|
|
|
|
|
|
URL, open it in your own browser, paste the code back); `codex` and `grok`
|
|
|
|
|
|
have their own login step. Works because the box is outbound-only; the tool
|
|
|
|
|
|
never handles a token.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
- **Git** — the operator adds their own PAT / `gh auth login` inside the box.
|
|
|
|
|
|
|
|
|
|
|
|
The tool stores and injects **no** credentials, ever. This dissolves the
|
|
|
|
|
|
multi-user problem: nothing shared, nothing committed.
|
|
|
|
|
|
|
|
|
|
|
|
## Snapshots are the reuse mechanism
|
|
|
|
|
|
|
|
|
|
|
|
Re-authing every fresh box would be toil, so authenticated state is reused via
|
|
|
|
|
|
snapshots, not a secrets store:
|
|
|
|
|
|
|
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
|
|
|
|
- `box snapshot <n> [label]` — checkpoint after login + clone.
|
|
|
|
|
|
- `box new --name <n2> --from <src>[/<snapshot>]` — clone an existing box
|
2026-07-10 15:00:36 +00:00
|
|
|
|
or snapshot (authed state and all). Isolation is preserved: the clone keeps
|
2026-07-15 00:23:58 +00:00
|
|
|
|
the `box-net` profile + `boxnet` + ACL.
|
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
|
|
|
|
- `box restore <n> <snapshot>` — roll a box back to a checkpoint.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
|
|
Log in once → snapshot → spin up authed boxes from it.
|
|
|
|
|
|
|
|
|
|
|
|
## The box announces itself to the agent
|
|
|
|
|
|
|
docs: extend the agent-agnostic reframe to design, recipe, and drill docs
Same treatment as the README, applied to the prose that stood in "Claude" for
"the coding agent": box-recipe.md and box-design.md now describe the `.box/`
runbook and creds-free flow around whichever agent the box was minted with, and
name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,
`~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's
"does not check" note says the drill confirms each template's CLI, not just
Claude Code. `claude` stays as the concrete login example throughout.
Left untouched (out of scope, literal identifiers): the legacy isolation-stack
names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template
files themselves, and drill/RUNS.md history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:21:22 +00:00
|
|
|
|
cloud-init installs a global agent-context file in every coding-agent box
|
|
|
|
|
|
(`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.grok/AGENTS.md`) telling the
|
|
|
|
|
|
agent it is running in a box (trust-less, ephemeral, creds-free) and to treat a
|
2026-07-14 18:01:34 +00:00
|
|
|
|
repo's `.box/` folder as its bootstrap runbook. No "tell it" step, no host
|
2026-07-10 15:00:36 +00:00
|
|
|
|
execution.
|
|
|
|
|
|
|
2026-07-14 18:01:34 +00:00
|
|
|
|
## `.box/` is optional, agent-facing documentation
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
|
|
|
|
|
Not host-executed shell. A repo that wants to be easy to stand up in a sandbox
|
|
|
|
|
|
ships a runbook (prose + optional scripts the agent may run). A repo that does
|
|
|
|
|
|
not, you set up by hand. The tool enforces no contract; there is no `install`.
|
|
|
|
|
|
|
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
|
|
|
|
## What box owns, and what it doesn't
|
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
|
|
|
|
|
2026-07-14 18:01:34 +00:00
|
|
|
|
Boxes are ordinary Incus instances, tagged `user.box=1`. That makes every
|
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
|
|
|
|
Incus verb a candidate feature request — `rename`, `info`, `file push`, on
|
|
|
|
|
|
forever — and wrapping them one at a time grows a worse `incus`. The rule:
|
|
|
|
|
|
|
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
|
|
|
|
> **box owns a command when it must enforce an invariant Incus cannot see:**
|
2026-07-14 18:01:34 +00:00
|
|
|
|
> the `user.box=1` boundary (never touch an instance we didn't mint), the
|
2026-07-15 00:23:58 +00:00
|
|
|
|
> isolation stack (`box-net` profile + `boxnet` + ACL), or the creds-free
|
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
|
|
|
|
> snapshot→clone workflow. Everything else is Incus's job.
|
|
|
|
|
|
|
|
|
|
|
|
The rule cuts both ways, and that's the point:
|
|
|
|
|
|
|
|
|
|
|
|
- `rename` **is** ours — not because it adds logic to `incus rename`, but because
|
|
|
|
|
|
resolving the name *is* the logic: check the tag, apply `--remote`, and notice
|
|
|
|
|
|
the box is running (Incus won't rename a running instance) so we can say "stop
|
|
|
|
|
|
it first" rather than leak an Incus error.
|
|
|
|
|
|
- `incus config set security.nesting=false` is **not** ours. It dismantles the
|
|
|
|
|
|
trust boundary; wrapping it would imply we bless it.
|
|
|
|
|
|
|
|
|
|
|
|
Two mechanisms keep this honest.
|
|
|
|
|
|
|
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
|
|
|
|
**The command table** (`CMDS` in `bin/box`) is the single source of truth
|
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
|
|
|
|
for what exists, its synopsis, its help line, its preconditions and what runs.
|
|
|
|
|
|
Dispatch and help are both rendered from it, so the help cannot describe a
|
|
|
|
|
|
command that doesn't exist — the failure that produced #8. A thin verb is one
|
|
|
|
|
|
row; a verb that can't be expressed as a row and enforces no invariant of ours
|
|
|
|
|
|
doesn't belong in the tool.
|
|
|
|
|
|
|
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
|
|
|
|
**The escape hatch** — `box incus <box> -- <args...>` — resolves and
|
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
|
|
|
|
tag-checks the box, then hands the rest to Incus verbatim. It means "no" to a
|
|
|
|
|
|
proxy request is not "you can't do that", and it keeps the one rail that matters:
|
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
|
|
|
|
you cannot aim it at an instance box didn't mint. If the command can move
|
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 off the isolation stack (profile, network, device, `security.*`), it
|
|
|
|
|
|
warns and proceeds — from there the trust boundary is yours to keep.
|
|
|
|
|
|
|
2026-07-14 01:29:33 +00:00
|
|
|
|
## Isolation
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
2026-07-15 00:23:58 +00:00
|
|
|
|
Dedicated NAT bridge `boxnet` + Incus `box-isolate` ACL dropping all
|
2026-07-10 15:00:36 +00:00
|
|
|
|
RFC1918/CGNAT/link-local egress, plus host-firewall rules blocking instance →
|
2026-07-14 01:29:33 +00:00
|
|
|
|
host. Entry is `incus exec` over the local socket — no inbound path. The VM is
|
|
|
|
|
|
the trust boundary.
|
|
|
|
|
|
|
|
|
|
|
|
**A box reaches the public internet and nothing else — including no other box.**
|
|
|
|
|
|
That last clause is the one that was assumed and turned out to be false, so it
|
|
|
|
|
|
is spelled out here with the mechanism, and `drill/` tests it on every run.
|
|
|
|
|
|
|
2026-07-15 00:23:58 +00:00
|
|
|
|
- **Box → host, LAN, RFC1918, CGNAT, link-local:** the `box-isolate` ACL.
|
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
|
|
|
|
- **Box → box: an nftables *bridge-family* rule** (`host/box-firewall.sh`).
|
2026-07-14 01:29:33 +00:00
|
|
|
|
It cannot be an ACL rule. Two boxes on one bridge share an L2 segment, so
|
|
|
|
|
|
their frames are *switched* between bridge ports and never traverse the
|
|
|
|
|
|
netfilter path an L3 ACL lives on — the ACL looked airtight (it drops
|
2026-07-15 00:23:58 +00:00
|
|
|
|
`10.0.0.0/8`, which contains `boxnet`) while box→box was in fact wide open.
|
2026-07-14 01:29:33 +00:00
|
|
|
|
A live probe found box A's SYN arriving at box B. The bridge family's forward
|
|
|
|
|
|
hook fires exactly on port-to-port frames, which on this bridge means box→box
|
|
|
|
|
|
and nothing else: gateway traffic and routed egress are delivered locally, not
|
|
|
|
|
|
forwarded. Dropping every forwarded frame therefore isolates the boxes and
|
|
|
|
|
|
costs them nothing.
|
|
|
|
|
|
- **Box → box by NAME:** `dns.mode=none`. dnsmasq on the gateway held a record
|
|
|
|
|
|
for every instance, so a box could enumerate its siblings even where it could
|
|
|
|
|
|
not reach them. Blocked connections with open reconnaissance is not isolation.
|
|
|
|
|
|
- **IPv6:** off (`ipv6.address=none`), and that is a *contract*, not a default —
|
|
|
|
|
|
every rule above is IPv4-only, so IPv6 would be an uncovered path.
|
|
|
|
|
|
- **`security.ipv4_filtering`: deliberately NOT used.** It breaks the box's
|
|
|
|
|
|
networking (in-box Docker cannot pull or run a container). Tested, vetoed.
|
|
|
|
|
|
|
|
|
|
|
|
The rule that keeps this honest: **isolation claims are tested, never reasoned
|
|
|
|
|
|
about.** The box→box hole existed because a plausible code reading said it could
|
|
|
|
|
|
not. See `drill/RUNS.md`.
|
2026-07-10 15:00:36 +00:00
|
|
|
|
|
2026-07-18 04:09:48 +00:00
|
|
|
|
## Multi-user hosts: access tiers
|
|
|
|
|
|
|
|
|
|
|
|
The daemon socket is binary — `incus-admin` holds everything on the machine —
|
|
|
|
|
|
so a shared host needs a second tier, and Incus ships one: **incus-user**
|
|
|
|
|
|
confines an `incus`-group member to an auto-created project `user-<uid>`,
|
|
|
|
|
|
behind a restricted certificate that cannot name any other project. The tier
|
|
|
|
|
|
is decided once, from the process's live credentials (`box_tier()`: UID 0 or
|
|
|
|
|
|
`incus-admin` → admin; `incus` alone → restricted; neither → none), and every
|
|
|
|
|
|
tier-aware verb reads that one function.
|
|
|
|
|
|
|
|
|
|
|
|
What incus-user does *not* do is honor box's contract — measured on Debian 13
|
|
|
|
|
|
/ Incus 6.0.4 (#74), after the design that assumed it (#72) was vetoed by its
|
|
|
|
|
|
own Task-0 rehearsal:
|
|
|
|
|
|
|
|
|
|
|
|
- it pins each user's project to a private auto-created bridge
|
|
|
|
|
|
(`incusbr-<uid>`) — a stock NAT bridge with **none** of the hardening: no
|
|
|
|
|
|
ACL, no `dns.mode=none`, no resolver pin, IPv6 on;
|
|
|
|
|
|
- it blocks snapshots — box's entire reuse workflow;
|
|
|
|
|
|
- the `box-net` profile lives in the default project, invisible to theirs.
|
|
|
|
|
|
|
|
|
|
|
|
So the tier is an **admin-run convergence** (`box grant <user>`), not a
|
|
|
|
|
|
group membership: put them in `incus`, touch incus-user once as them (the
|
|
|
|
|
|
project is created lazily; nothing exists to converge until it does), then
|
|
|
|
|
|
rewire the project — network access narrowed to `boxnet` **and only
|
|
|
|
|
|
`boxnet`**, snapshots allowed, the shipped profile installed. Narrowing is
|
|
|
|
|
|
the load-bearing decision: granting `boxnet,incusbr-<uid>` (the obvious fix)
|
|
|
|
|
|
would leave an unhardened NAT bridge one `--network` flag away from any box
|
|
|
|
|
|
they mint. With the private bridge unreferenced (its `eth0` is removed from
|
|
|
|
|
|
their default profile) and outside `restricted.networks.access`, the hardened
|
|
|
|
|
|
network is not their default placement — it is the only placement their
|
|
|
|
|
|
certificate can express. The grant survives incus-user restarts by that
|
|
|
|
|
|
tool's own design (it configures a project only at creation), and a restricted
|
|
|
|
|
|
certificate cannot widen its own project — both measured, not read.
|
|
|
|
|
|
|
|
|
|
|
|
Cross-USER isolation is the same mechanism as cross-box isolation, on
|
|
|
|
|
|
purpose: their instances share `boxnet` with everyone's, and the bridge-family
|
|
|
|
|
|
drop + port isolation + `dns.mode=none` already make any two boxes strangers.
|
|
|
|
|
|
A restricted user CAN strip `security.port_isolation` from the profile copy
|
|
|
|
|
|
in their own project — which is why the nft bridge drop, which they cannot
|
|
|
|
|
|
touch, exists as the second, host-owned layer. Defense in depth, both layers
|
|
|
|
|
|
measured (`drill/multiuser.sh`, criteria a–l).
|
|
|
|
|
|
|
|
|
|
|
|
`box revoke` is two strengths: bare, it removes the group — the socket
|
|
|
|
|
|
closes, their boxes keep *running* (revoking a person does not kill their
|
|
|
|
|
|
workloads) and `grant` restores everything; `--purge` deletes their world
|
|
|
|
|
|
(boxes, images, project, private bridge, trust-store certificate) and asserts
|
|
|
|
|
|
the absence afterwards.
|
|
|
|
|
|
|
2026-07-10 15:00:36 +00:00
|
|
|
|
## Non-goals
|
|
|
|
|
|
|
2026-07-18 04:09:48 +00:00
|
|
|
|
- Interactive-first: install and setup prompt by default (`BOX_YES=1` and the
|
|
|
|
|
|
CI rehearsal job are the sanctioned unattended paths).
|
2026-07-10 15:00:36 +00:00
|
|
|
|
- No credential storage or injection by the tool.
|
2026-07-18 04:09:48 +00:00
|
|
|
|
- No per-user resource quotas on the restricted tier (Incus's
|
|
|
|
|
|
`limits.*`/`restricted.*` project keys exist when someone needs them).
|