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
|
|
|
|
|
|
|
|
## Non-goals
|
|
|
|
|
|
|
|
|
|
- No unattended/CI bring-up — the flow is interactive.
|
|
|
|
|
- No credential storage or injection by the tool.
|