Usage#
The core worktree lifecycle is five commands:
treebox doctor # 0 · is this host ready?
treebox create # 1 · provision + launch (name generated)
treebox enter brave-otter --harness codex # 2 · come back later
treebox list # 3 · what exists, what's stale
treebox teardown brave-otter # 4 · clean up (variadic: name several)
Two commands sit alongside the lifecycle: template
scaffolds and inspects the docker sandbox templates, and version prints the
installed version.
Worktree commands run against the current repo; pass --repo PATH (absolute,
relative, or ~-prefixed) to point elsewhere, or --root DIR (repo-relative,
absolute, or ~-prefixed) to override the worktree root for this invocation.
treebox version, treebox --version, and treebox -V print the installed
version.
create — provision and launch#
treebox create # generated name (brave-otter), placeholder branch
treebox create fix-auth # named up front: works on branch fix-auth
treebox create feature/auth # slash names: branch feature/auth, dir feature--auth
treebox create --checkout feature/auth # exact existing branch (resume work, PR review)
treebox create fix-auth -- -p "Fix the login race" # launch the agent on a task
Fetches origin, cuts a worktree from the fresh origin/main, copies your
.env and submodules, syncs dependencies from the shared cache, records the
lockfile hash, and launches the agent.
An explicit NAME is intentional, so it is the branch, created fresh
from origin/<base>: slug tokens joined by slashes
(fix-auth, feature/user-auth), with the slashes flattened to -- in the
directory. Use the flattened name when
selecting a worktree.
The treebox/ prefix is reserved for generated
placeholders and rejected as a name.
You never have to invent a branch name up front, though. Omit NAME and
treebox generates a petname, and the worktree starts on a placeholder
branch treebox/<petname>. Placeholders are un-pushable by design:
every worktree carries a per-worktree pre-push guard that rejects any
treebox/* ref with instructions to name the work first —
✗ treebox: refusing to push placeholder branch 'treebox/brave-otter'.
↳ name this work first, then push again: git branch -m <type>/<short-name>
↳ we use conventional-commits style branch names — pick the type that fits
the change: feature/user-auth, fix/login-race, chore/bump-deps,
docs/api-guide, refactor/db-layer, test/flaky-suite, …
↳ the treebox/ prefix marks auto-generated names; PRs should never carry them.
The guard's suggestion mirrors conventional-commit types, so the branch name an
agent picks (feature/user-auth, fix/login-race, chore/bump-deps, …)
describes the change the same way its commit subjects do.
The branch is a mutable attribute: rename it whenever the work takes shape
(git branch -m fix/flaky-teardown) — the directory never moves, and list /
enter / teardown follow the rename automatically. git push --no-verify
remains the deliberate human escape hatch.
Anything after -- goes to the agent verbatim, exactly as for
enter: create NAME -- ARGS starts the
agent on a task, and with --json or --print the args are part of the
emitted entry_command. Agent args need a NAME (a nameless create takes
none).
--checkout checks out an existing branch (local or origin/) exactly —
for resuming work or reviewing a PR. The worktree's name derives from the
branch (feature/auth → feature--auth; a placeholder drops its treebox/
prefix first, so treebox/a/b → a--b) unless you pass NAME too. It is
also the deliberate way to resume a branch you own: create NAME for a branch
that already exists (locally or on origin) is a loud BRANCH_EXISTS conflict,
never a silent reuse of old work.
Sandbox any of these by picking docker isolation:
treebox create fix-auth --isolation docker
| Option | Effect |
|---|---|
--checkout BRANCH |
Check out this exact existing branch instead of creating a new one. |
--base BRANCH |
Base for the new branch — any branch, not just main. Resolved as origin/<base>. |
--isolation MODE |
host (default) or docker. |
--harness, -H NAME |
Agent harness to launch: claude or codex. |
--cold |
Bypass shared caches for a from-source build. |
--no-fetch |
Opt out of the required origin fetch and accept possibly stale refs. |
--firewall/--no-firewall |
Enable/disable the container firewall (docker isolation). Unset: the config default applies. |
--template NAME |
Operator-owned sandbox template to render. |
--dry-run, -n |
Print a stable command plan; change nothing. |
--print |
Provision, then print the launch command instead of launching. |
--json |
Provision, then print a JSON result instead of launching. |
A name that already exists is a loud conflict (exit 5,
error.code: "SLUG_CONFLICT") with the ways out — enter it, teardown it,
or pick another name. It is never silently reused. If a previous create did
not finish setup (it was interrupted, or a setup step failed), running the same
create again finishes setup instead; a fully provisioned same-name worktree
is still a conflict.
A failed setup is a failed create. When a setup step (a package-manager
install, a setup_hook command, the docker image build, the firewall, or
postCreate) exits non-zero, create exits 1 with
error.code: "SETUP_FAILED". The message names the step and its exit status;
error.path is the file that holds the step's output
(<worktree-parent>/.logs/<name>.setup.log, outside the worktree). The
worktree is kept and its setup is recorded as failed, so treebox enter <name>
or the same create retries the setup. It never says ready on a failed setup.
Submodule copies and recovery#
During create, including retries, treebox merges checked-out submodule
contents from the source repo into the worktree. It preserves regular file
contents and permissions, including executable bits. Source symlinks, including
dangling links, are copied as links. Entries named .git are excluded at every
depth, and .git cannot be a submodule path component. Destination entries
that have no matching source entry are kept.
The worktree's own branch decides which submodules it gets. Treebox reads the
branch's .gitmodules and never writes that file, so a fresh worktree is clean
even when its branch renamed, added, or removed a submodule. A path is copied
only when the main checkout declares the same path and has it on disk. Treebox
skips every other path with a note (skipped <path> · not checked out in
<repo>, not printed under --json) and leaves that directory as the branch
has it. A branch without a .gitmodules gets no submodule trees. Treebox does
not run git submodule update: the copied content mirrors the main checkout,
not the commit that the branch pins.
Existing destination symlinks and hardlinks at file locations are replaced without writing to their targets. Paths to destination directories must contain real directories. A symlink or file in a directory path, a directory blocking a source file or link, or a directory that changes during copying stops creation. Treebox does not remove conflicting directories.
A copy failure identifies the affected path and gives a recovery hint. Inspect
that path, move any conflicting entry aside, then retry create with the same
worktree name and branch. Files copied before the failure can remain; the retry
merges the contents again. enter does not copy submodules. See the
scripting contract for error codes and JSON fields.
Stacking on another branch#
--base isn't limited to main. Point it at dev, a release branch, or an
existing PR's branch to build on top of it in a fresh worktree:
treebox create auth-fixes --base feature/auth
This works even while feature/auth is checked out in another worktree — the
new worktree checks out its own new branch, so git's
one-checkout-per-branch rule never triggers. The base resolves as the freshly
fetched origin/feature/auth (falling back to the local branch only if it was
never pushed), so push the base first if its latest commits only exist locally.
Not sure what it will do? Ask first: --dry-run (-n) prints a stable plan
of the git / setup commands:
$ treebox create fix-auth --dry-run
● create fix-auth · dry run
worktree .treebox/worktrees/fix-auth
branch fix-auth
isolation host → claude
$ git -C ~/code/myapp fetch origin --quiet
$ git -C ~/code/myapp worktree add -b fix-auth ~/code/myapp/.treebox/worktrees/fix-auth origin/main
# install pre-push guard: per-worktree core.hooksPath -> <private git dir>/treebox-hooks (treebox/* refs are un-pushable)
$ cp .env ~/code/myapp/.treebox/worktrees/fix-auth/.env
$ uv sync
A dry run checks repository, worktree registration, and template preconditions.
It reports the corresponding create exit code and JSON error -
BRANCH_EXISTS for a name whose branch already exists,
SLUG_CONFLICT for an occupied worktree directory, NOT_FOUND for a missing
--checkout or base branch, BRANCH_IN_USE for a --checkout branch already
backing another worktree, TEMPLATE_NOT_FOUND for a docker --template that
doesn't resolve, TEMPLATE_INVALID for one that resolves but whose
container.json (or firewall.json with --firewall) is broken.
As with real create, a half-built worktree from an interrupted run previews
finishing setup (no fetch, no worktree add) instead of conflicting. Dry-run
verdicts reflect the refs already available locally; run git fetch origin
first when exact parity with create's normal fetch is required. The setup plan
detects package manifests from the exact locally available --base or
--checkout revision that it will materialize; when it resumes an interrupted
create, it detects them from the existing worktree. The submodule part of the
plan comes from the same revision's .gitmodules (or the existing worktree's),
with the same rule as the real run: it names each path it would copy and each
path it would skip. A dry run does not copy submodules or inspect their
destination paths, so it cannot detect
submodule copy conflicts.
It also skips the runner preflight, including the
Docker user requirement.
Nothing on disk or in git changes.
With --cold --dry-run, the cache part of the plan also matches the real cold
setup without creating its disposable cache. A host plan uses the stable
<temporary-dir>/treebox-cold-XXXXXXXX placeholder in cache environment
values and package-manager cache flags. A plan for a new docker container
omits the shared package-cache mounts that treebox injects and their cache
environment variables.
If an interrupted create already left a docker container behind and the Docker
daemon is reachable, the plan shows what real setup will do with it - reuse it
(or docker start it) instead of building and running a new one - warm or
cold. Under --cold it also notes that the existing container keeps its
creation-time cache mounts. This is a read-only engine query; with the daemon
down, the plan falls back to the build-and-run commands.
enter — come back to a worktree#
treebox enter fix-auth --harness claude
treebox enter fix-auth --harness codex -- --resume
enter and teardown select from registered linked worktrees below the
configured root, including nested directories. The main checkout is excluded.
They take a ref, matched in this order: exact worktree name, exact current
branch, then a unique substring of either. Branches are read live from git,
so a renamed branch still works. An ambiguous ref exits 2 and lists the
matches. If nested worktrees share a name, use their distinct branch refs or
set --root to the target's parent directory. Operations then follow the
selected worktree path.
If the ref names a branch that exists but has no worktree (never materialized,
or torn down), the NOT_FOUND
error hints at treebox create --checkout REF instead of the generic advice.
Re-enters the worktree and launches the agent. By default it reuses the
harness the worktree was provisioned with (create -H codex then plain
enter launches codex, not the config default); an explicit --harness is a
per-session override that launches that agent this time without changing what's
recorded on disk. All recorded choices come from the worktree's
host-owned record, never from files a
sandboxed agent can write. The sandbox template is reused the same way: a worktree
created with --template node re-renders that template on enter, not the
config default, and an explicit --template is a per-session override. The
recorded isolation mode also wins over the config default; a conflicting
explicit --isolation exits 5 instead of entering the wrong kind of
worktree. For docker worktrees, the recorded firewall choice is reused too, so
create --no-firewall keeps entering cleanly even under a firewall = true
config.
Dependencies re-sync only if the lockfile changed since the last setup
finished (treebox stores the hash with the setup result in the host-owned
record; a re-sync never changes the recorded policy). If a previous setup was
interrupted or failed, enter runs the setup again whatever the hash, instead
of launching into a half-built tree; a setup step that fails again makes
enter fail with SETUP_FAILED (exit 1). A worktree whose .git pointer is
missing or leads elsewhere is refused with INVALID_LINKAGE (exit 5; the
hint points at teardown). Anything after -- is passed through to the
agent. --cold forces a cache-bypassing re-sync.
Under docker isolation, enter runs the runner preflight first (like
create and doctor), so a stopped daemon fails fast with a clean
DOCKER_UNAVAILABLE error and a start-docker hint rather than a misleading
error deeper in launch. For user requirements and recovery steps, see
Install.
It also re-stages the scoped credential copies
from the host on every entry - independent of the lockfile-hash skip - so a
host logout/revocation drops the stale copies and a fresh login reaches the
sandbox on the very next entry. The same refresh runs during create.
Each staged file is replaced atomically: staged symlinks and hardlinks cannot
redirect credential writes or permission changes to host files. A symlink at
the staged directory itself is rejected. Conflicting directories are preserved.
Both conditions stop create or enter with an
ERROR failure; inspect their contents before
removing the conflict.
Docker entry and firewall recovery#
treebox restarts a stopped container before entry. If the container was
created with the firewall enabled, treebox runs init-firewall.sh on every
entry, including entries to running containers. This step occurs before the
agent starts or treebox emits a --print or --json command. It restores
rules lost after a container restart outside treebox.
If initialization fails during setup or entry, treebox exits with
FIREWALL_FAILED (exit 1). It does not start the agent or emit an entry
command. The bundled script drops traffic on failure. Its allowlist fetch
then cannot succeed on another entry.
Inspect the operator template named in error.hint. If the hint includes a
teardown and create command, commit or back up uncommitted work before you
use it. The command checks out the current branch and preserves the worktree
path, template, firewall choice, and harness.
If the CLI cannot restore the same path and branch, the hint gives no teardown command. Keep the worktree and repair the sandbox manually. If no branch is available, save the current work on a branch first.
list — what exists, what's stale#
(treebox ls works too.)
$ treebox list
NAME BRANCH ISOLATION LAST COMMIT AGE DEPS ENV
──────────────────────────────────────────────────────────────────────────────────────
fix-auth fix/ci-caching docker cache uv wheels in CI 2h ● fresh ● present
brave-otter ⚠ unnamed host initial provision 3d ● stale ● present
Summary: 2 worktrees · 1 unnamed · 1 stale
Root: /home/me/project/.treebox/worktrees
Rename unnamed: git branch -m <type>/<short-name>
Sorted by recency — the worktree you want is almost always the one that just
committed. The name is the stable handle; the branch is read live.
A worktree's isolation is read from its host-owned record;
worktrees without a record (for example made by an older treebox)
or worktrees with an invalid record show unknown
instead of being mislabeled with the current config default. The muted
Root footer shows the discovery root. It does not show the intermediate
directories of nested worktrees; use list --json for each full path.
On a narrow terminal, the commit-subject column is omitted first so
identity, isolation, age, dependency freshness, and .env state stay legible.
If name and branch still cannot fit side by side, the branch moves below its
stable name instead of either value being truncated.
A placeholder branch is always treebox/<name>, so its row just shows
⚠ unnamed — work that can't be pushed yet is visible at a glance. stale
means the lockfile changed since the last dependency sync — the next enter
fixes that automatically. A setup that did not finish shows
◐ setup unfinished or ✗ setup failed in the DEPS column, and a worktree git
cannot use shows its health
(⚠ directory missing, ⚠ invalid linkage) there instead of a freshness it
cannot have.
teardown — clean up#
(treebox rm works too.)
treebox teardown # pick interactively (no refs)
treebox teardown fix-auth # remove one worktree
treebox teardown fix-auth brave-otter --force # several at once
treebox teardown fix-auth --delete-branch # ...and its local branch
treebox teardown fix-auth --yes --json # unattended: confirm, keep the dirty check
Takes one or more refs (name, branch, or unique substring). Every ref is
resolved before anything is removed — a typo among three targets removes
nothing. Refuses to remove a worktree with uncommitted changes unless you pass
--force. It asks for confirmation in a terminal; unattended (--json,
--non-interactive, or no terminal) there is no prompt
and it refuses with NEEDS_CONFIRMATION (exit 5) unless you pass --yes.
--yes only answers the prompt: a worktree with uncommitted changes is still
refused (DIRTY_WORKTREE, exit 5); --force removes it anyway. For
docker-sandboxed worktrees, --remove-volumes also removes
per-workspace volumes authorized by the
runner-owned manifest written outside
the sandbox mount at create time, so they are still removed when the container
and user template were already deleted. If that manifest is missing or
invalid, including on older worktrees or after manual deletion of the
rendered config directory, treebox warns and skips volume removal
instead of guessing. --skip-container leaves containers/images untouched.
--json prints a structured record of what was removed (and never blocks on
a prompt).
An incomplete cleanup is a failed teardown. Teardown follows the
cleanup model: runner resources
(container, image, volumes, sandbox files) first, then the worktree directory
and its git registration, then the ownership record. When a step cannot
finish - a container, image or volume removal fails, Docker is not reachable,
or git refuses to remove the worktree - teardown exits 1 with
error.code: "TEARDOWN_INCOMPLETE", names what remains, and keeps the
worktree, its host-owned record and the runner's resource manifest. Run the
same teardown again when the cause is fixed: it retries the remaining steps.
--skip-container is the explicit way to leave docker resources alone.
Teardown checks for uncommitted changes and git locks before the prompt and
again under the worktree lock, right before it removes anything: a file written
while the confirmation prompt is open is never deleted. treebox never removes a
worktree that git refuses to remove, and never a worktree locked with
git worktree lock (WORKTREE_LOCKED, exit 5, even with --force). Local
branch deletion stays best-effort: if it fails, teardown warns and reports
branch_deleted: false without undoing the worktree removal.
Run it with no refs and treebox walks you through the whole decision — an
arrow-key picker (↑↓ to move, space to toggle, enter to confirm) over your
worktrees, each annotated with a "will I lose work?" badge; a second, smaller
picker for which of those also lose their local branch; then the removal:
$ treebox teardown
? Select worktrees to tear down
» ◉ calm-finch feat/api 3d ✓ merged (PR #42)
◯ brave-otter fix/login 1d ⇡ ahead 2 · unmerged
◉ trusty-robin ⚠ unnamed 2h ✎ uncommitted · never pushed
? Also delete the local branch for … (space to pick, enter to skip)
» ◉ calm-finch feat/api 3d
◯ trusty-robin ⚠ unnamed 2h
● teardown calm-finch
· container n/a (host isolation)
✓ worktree removed .treebox/worktrees/calm-finch
✓ branch deleted feat/api
✗ trusty-robin kept · uncommitted changes
↳ commit or stash the changes, or re-run teardown with --force
Removed 1 worktree · kept 1 with uncommitted changes.
The badge starts with local Git facts: uncommitted changes, commits ahead of
the upstream, whether the current commit is in the base, and whether an
upstream is set. These checks work with any remote (GitHub, GitLab,
Bitbucket, plain git) and any auth. A freshly created worktree with no commits
of its own shows ⚠ empty (safe to delete when clean). Its tip is already in
the base, but no work has been merged. If a forge CLI you're already logged into is present
(gh for GitHub, glab for GitLab), the picker also shows the branch's PR/MR
state. With no CLI, an unknown host, or no connection, the picker falls back
to the local badge.
A merged PR/MR proves a squash merge only when its recorded head commit
matches the worktree's current commit. New local or pushed commits on a reused
branch do not inherit the old request's green badge. If a merged request is
found but the current commit cannot be read, the commits differ, or the
request has no complete, valid head commit ID, the picker shows
merge not verified for current tip unless local Git independently proves
the current commit is already in the base. GitLab uses the newest request for
the branch, so an old merged request does not hide a newer open one. Dirty
worktrees remain unsafe even when their committed work has merged.
The branch question is per-worktree, not one switch for the batch — drop a
merged branch and keep an in-progress one in the same pass, so
--delete-branch isn't something you have to remember up front. Ctrl+C at
either question backs out of the whole run with nothing removed.
Picking in the chooser stands in for the confirm prompt, and a mixed
selection just works: the clean worktrees are torn down while any with
uncommitted changes are kept and reported (like trusty-robin above), so
one dirty tree never blocks the rest. treebox exits 5 when it kept anything,
so a script can still tell the run wasn't total. (Passing refs explicitly stays
all-or-nothing — naming a dirty tree stops the whole run. --force removes
dirty worktrees either way.) The chooser is interactive-only: under --json,
--non-interactive, or a non-TTY, pass refs explicitly (no refs is
NEEDS_CONFIRMATION, exit 5).
teardown is also the recovery path for a corrupt worktree - a registered
directory whose .git pointer file is gone (say, after an interrupted removal
by hand). Git commands inside such a directory silently answer for the main
checkout, so create and enter both refuse it loudly (exit 5;
error.code is "SLUG_CONFLICT" for create and "INVALID_LINKAGE" for
enter) with a hint pointing here rather than running
setup or an agent against your real checkout. teardown instead verifies the
worktree's git linkage before asking whether it is dirty: a corrupt worktree is never blocked as "dirty" just because your
main checkout has uncommitted changes. It takes the normal confirmation path
instead (the interactive prompt, or --yes under --json / non-TTY), and
removal clears both the directory and git's stale registration without
touching the main checkout's files. Container cleanup survives the corruption
too: teardown reads the worktree's isolation mode from its host-owned
record, which lives beside the worktree and does not depend on the .git
pointer, so a corrupt docker worktree is still torn down with the runner it was
created with. Volume ownership comes separately from the host-owned resource
manifest and is never read from sandbox-writable state. Teardown removes the
record only after the worktree and its runner resources are gone.
teardown is also the recovery for a worktree without a usable record
(RECORD_MISSING / RECORD_INVALID on enter, for example one made by an
older treebox): it needs no extra flag. Without a record, a worktree that has
the runner-owned resource manifest is torn down as a docker worktree (its
container, image and manifest go too), and any other as a host worktree. Then
treebox create it again. The dirty-worktree and confirmation rules above
apply as usual, and so does TEARDOWN_INCOMPLETE: the manifest is kept until
every docker resource is gone, so the same plain teardown retries.
doctor — is this host ready?#
treebox doctor # checks for host isolation
treebox doctor --isolation docker # also checks the Docker daemon
treebox doctor -H codex # check codex instead of the config default
Checks git, the agent and its login, .env, and - because create requires
a fresh fetch - whether git can authenticate to origin without a prompt. A
missing .env is not a failure: the row renders as a muted · note marked
"optional" (the configured path still shown), since create simply skips the
copy. Every failing row comes with the command that fixes it.
For the selected harness (--harness, else the config default: the one
create launches) doctor checks what a launch needs, where the agent runs:
| Row | Host isolation | Docker isolation |
|---|---|---|
agent: <harness> |
The harness binary (claude, codex) is on PATH. |
Part of the sandbox image: always ok here, checked in the container at launch (HARNESS_MISSING). |
login: <harness> |
A real login: the login file (~/.claude/.credentials.json, ~/.codex/auth.json) holds a JSON object or, when that file is absent on macOS, Claude Code's Keychain item does. |
The same login: the sandbox gets a copy of it as the login file. |
A login dir that merely exists (settings, history) is no login. The other
harnesses' login rows are shown as optional. Hard failures (git, repo,
the selected agent and its login, or the selected isolation mode) exit 1 in
both human and --json modes (ok: false), so
treebox doctor --json && treebox create ... works in scripts.
template — scaffold and inspect sandbox templates#
Docker isolation renders an operator-owned template — a Dockerfile +
container.json (and the firewall scripts) — into a host-side dir beside the
worktree. The template command is the sanctioned way to fork and inspect those
templates; it works from any install, so customizing a sandbox never means
reaching into the package internals. Named templates live under
$TREEBOX_HOME/templates/<name> (see configuration).
treebox template init node # copy the built-in default → ~/.treebox/templates/node, then edit
treebox template init node --from python # fork one of your own instead
treebox template list # names, source, firewall support, content status, and the config default (ls works too)
treebox template path node # print the resolved dir: cd "$(treebox template path node)"
template init <name> copies the source template before it installs the new
directory. It refuses to replace an existing template without --force.
With --force, treebox moves the old template aside and installs the new copy
with renames. A copy failure or a failure to move the old template leaves the
old template unchanged. If installation fails, treebox renames the old template
back. If rollback also fails, the old template remains unchanged at the path
in the error message.
If cleanup fails after installation, the command succeeds and keeps the complete
new template. Human output includes a warning on stderr with the leftover path.
--json keeps the existing success payload and has no cleanup warning field.
Each run uses a unique directory under $TREEBOX_HOME/templates/.treebox-tmp.
template init rejects .treebox-tmp as a name, and template list hides that directory.
Treebox leaves directories from other runs untouched because another process
can still use them. Leftovers from an interrupted run can coexist with later
runs. Before you remove a leftover directory, make sure that no process still
uses it.
Then edit the Dockerfile and container.json and launch with
treebox create <name> --isolation docker --template <name>. If the source lacks
required files, template init copies it and reports the missing files.
template list flags any template dir missing a required file or carrying malformed JSON
before create does, shows whether each template can serve --firewall
(a firewall.json is optional), and marks which one is your config default. template path [<name>] (default: default)
prints the resolved location — the install-agnostic answer to "where does this
template live", including the bundled default. All three take --json for
scripting.
Scripting against treebox#
treebox is built to be driven by other programs (including agents). Data goes to stdout, diagnostics to stderr, and exit codes are stable:
| Code | Meaning |
|---|---|
0 |
ok |
1 |
runtime failure, missing runner dependency, or failed doctor hard check |
2 |
usage — invalid name/branch, ambiguous ref, bad option |
3 |
not found — the worktree/branch/template doesn't exist |
4 |
required fetch or its authentication failed |
5 |
conflict — name taken, dirty tree, or lock held |
create and enter that launch the agent exit with the agent's exit code
instead, and a cancel signal exits 128 + signal; see
unattended use for how a caller tells the two apart.
The worktree commands and doctor take --json; payloads carry a
schemaVersion — fields are only ever added within a version, and a breaking
reshape or rename bumps it. Agents branch on these payloads, so treat the shape
as a contract. Request JSON output with:
$ treebox create fix-auth --json
For a complete result, see the generated
create --json snapshot.
With --json (or --print) treebox provisions but does not launch the
agent — it hands you the launch command instead, so your script decides when
to run it. The command is self-contained for both isolation modes: it carries
the worktree directory (cd … && exec … on host, docker exec -w … in
docker), so replaying it from any directory launches the agent in the box.
Before it prints the command, treebox completes the
Docker entry checks.
Without --json, --print, or --dry-run, create and enter launch the
agent and exit with the agent process's exit code (see
the agent exit line). Human --dry-run writes the
would-run plan to stderr; --dry-run --json writes its payload to stdout.
Current success payloads:
| Command | Top-level fields |
|---|---|
create --json, enter --json |
schemaVersion, name, worktree_path, branch, base, entry_command, created, warnings |
create --dry-run --json |
schemaVersion, dry_run, name, worktree_path, branch, commands, warnings |
list --json |
schemaVersion, worktrees |
teardown --json |
schemaVersion, worktrees, warnings |
doctor --json |
schemaVersion, ok, isolation, checks, advisories |
warnings is a list of strings: every warning the run printed (or, under
--json, would have printed) on stderr, such as a .env copy skipped because
the worktree has a directory at .env, or a teardown step that warned. It is
[] when nothing was skipped, so a caller can branch on a non-empty list
instead of parsing stderr.
list rows contain name, branch, unnamed, missing, last_commit,
commit_epoch, path, base, isolation, harness, deps, env,
health, and setup. deps is fresh, stale, or unknown: fresh and
stale only after a finished setup (a matching lockfile hash alone is never
fresh); env is present or absent. setup is the
setup model state - pending,
failed, or done - or "" when the record is missing or invalid. health
is the one classification create, enter, list and teardown share:
health |
Meaning | missing |
|---|---|---|
healthy |
Registered, linked to its own git registration, record readable. | false |
directory_missing |
The directory is gone; git may still list it. teardown prunes it. |
true |
invalid_linkage |
Registered, but its .git pointer is missing or leads elsewhere. enter refuses (INVALID_LINKAGE); teardown recovers it. |
true |
unregistered |
A directory git has no registration for (never listed; seen by create). |
false |
unreadable_state |
Git is fine, but the host-owned record is missing or invalid (RECORD_MISSING / RECORD_INVALID). |
false |
missing is kept for older consumers: it is true exactly when health is
directory_missing or invalid_linkage (only the git registration is
reliable), the same cases older treebox versions reported.
teardown records contain name, branch, worktree_path, removed,
branch_deleted, container, volumes_removed, complete, and remaining;
container is cleaned, skipped, or failed. Each field reports what the
runner actually did to that resource: container is skipped when cleanup
didn't run (--skip-container, or Docker unavailable) and failed when a
container or image removal failed, while volumes_removed is true only when
docker volumes were really removed - never on host isolation, not merely
because --remove-volumes was passed, and a failed volume removal leaves it
false without marking container failed. complete is false when a
cleanup step did not finish; remaining then names each resource that is
still present ("<resource> (<reason>)") and the command exits 1
(TEARDOWN_INCOMPLETE) after the payload. removed is true only when the
worktree directory was removed in this run. doctor checks contain name,
ok, and detail.
JSON errors are emitted to stderr as:
{
"schemaVersion": 1,
"error": {
"code": "NOT_FOUND",
"message": "...",
"hint": "..."
}
}
error.code and error.message are always present; error.hint and
error.path appear when treebox has useful remediation or a path-specific
failure. Both submodule copy errors include error.path and error.hint.
Under --json, stderr holds exactly this one object: a failed step's output
is never printed there, but kept in the file error.path names
(SETUP_FAILED); --verbose streams step output to stderr (never stdout) if
you want it live.
error.code and the exit code are the scripting contract:
error.code |
Exit | Common trigger |
|---|---|---|
INVALID_CONFIG |
2 |
Bad config file or invalid --isolation / --harness / --template (a template name is an identifier, never a path). |
INVALID_NAME |
2 |
create NAME is not slash-separated lowercase slugs, or uses the reserved treebox/ prefix; or a template init / template path name is not a template identifier, or template init names the reserved staging name. |
INVALID_BRANCH |
2 |
create NAME or --checkout names an invalid git ref. |
NOT_A_REPO |
2 |
--repo is not a git repo. |
INVALID_ROOT |
2 |
The worktree root (--root or root in config), or its nearest existing parent, is not a directory. Every command that takes --root checks it. |
AMBIGUOUS_REF |
2 |
A ref matches more than one worktree. |
NOT_FOUND |
3 |
The requested worktree, checkout branch, or base branch does not exist. |
FETCH_FAILED |
4 |
Required fetch/auth failed. |
GIT_MISSING |
1 |
git itself is not installed or not on PATH. |
MISSING_DEPENDENCY |
1 |
Required runner dependency is missing. |
DOCKER_UNAVAILABLE |
1 |
Docker is installed but the daemon is unavailable. |
HARNESS_MISSING |
1 |
A launch found no harness binary: not on PATH (host, checked before anything is provisioned) or not in the sandbox image (docker, checked at launch). doctor reports it as agent: <harness>. |
LOGIN_MISSING |
1 |
An unattended launch found no usable login for the harness (the agent would wait in its own login flow). Checked before anything is provisioned; doctor reports it as login: <harness>. |
RUNNING_AS_ROOT |
1 |
create or enter requested Docker isolation as root (uid 0). See the user requirement and recovery steps. |
SUBMODULE_COPY_FAILED |
1 |
An I/O or permission error prevents the submodule copy. |
SETUP_FAILED |
1 |
A setup step exited non-zero. The message names the step and its exit status; error.path is its output. The worktree is kept, marked unfinished; enter or the same create retries the setup. |
TEARDOWN_INCOMPLETE |
1 |
A teardown step did not finish (a container, image or volume removal failed, Docker is not reachable, or git refused to remove the worktree). The message names what remains; the worktree and its ownership record are kept, so the same teardown retries. |
ERROR |
1 |
Runtime failure, including a staged credential refresh failure. |
CANCELLED |
128 + signal |
A cancel signal (SIGINT 130, SIGTERM 143, SIGHUP 129) reached treebox while the agent ran; the agent was stopped. |
DESTINATION_CONFLICT |
5 |
An unsafe, conflicting, or changed path prevents the submodule copy. |
SLUG_CONFLICT |
5 |
The worktree name is already taken, or its directory is not a healthy registered worktree - recover a registered-but-corrupt one with treebox teardown <name>; a stray directory git never registered is invisible to teardown and must be removed by hand (rm -rf it). |
BRANCH_EXISTS |
5 |
create NAME names a branch that already exists — resume it with --checkout. |
BRANCH_IN_USE |
5 |
The --checkout branch is already checked out in another worktree. |
INVALID_LINKAGE |
5 |
enter target's .git pointer is missing or does not lead to its git registration (health: invalid_linkage) - recover with treebox teardown <name>. |
DIRTY_WORKTREE |
5 |
Explicit teardown target has uncommitted changes (checked before the prompt and again right before removal). |
WORKTREE_LOCKED |
5 |
Teardown target is locked with git worktree lock; treebox never removes it (also with --force). Unlock it first. |
MAIN_WORKTREE |
5 |
A teardown target resolves to the repo's main working tree (possible when the worktree root contains the repo, e.g. root = "..") - treebox only manages linked worktrees and never removes the repo itself. |
NEEDS_CONFIRMATION |
5 |
Teardown would need an interactive choice or confirmation. |
LOCK_HELD |
5 |
Another treebox operation holds this worktree's lock. Also raised, rarely, when the repository lock that parallel create / teardown calls queue on has been held for over five minutes: another operation is stuck, not merely busy. |
ISOLATION_MISMATCH |
5 |
Explicit --isolation disagrees with the recorded mode. |
RECORD_MISSING |
5 |
enter: the worktree has no host-owned record (made by an older treebox, or the record was deleted) - treebox teardown <name>, then treebox create again. |
RECORD_INVALID |
5 |
enter / resumed create: the host-owned record does not parse (another schemaVersion, an unknown isolation mode or harness, or bound to another path) - treebox teardown <name>, then treebox create again. |
RECORD_CONFLICT |
5 |
A resumed create of a half-built worktree asks for different settings than its record holds - re-run with the recorded settings from the hint, or tear it down. |
TEMPLATE_NOT_FOUND |
3 |
A named template doesn't exist - template init --from / template path, or create / enter provisioning (including --dry-run) with a template that doesn't resolve. |
TEMPLATE_INVALID |
1 |
A template exists but its contents can't serve the run: malformed or unreadable container.json, or --firewall with a template that has no firewall.json. Inspect it with treebox template path <name>. |
FIREWALL_FAILED |
1 |
The sandbox firewall failed to initialize. See firewall recovery. |
TEMPLATE_EXISTS |
5 |
template init names an existing template — pass --force to overwrite. |
TEMPLATE_CONFLICT |
2 |
template init source and destination are the same template. |
OVERWRITE_FAILED |
1 |
template init --force cannot move the existing template aside. |
COPY_FAILED |
1 |
template init cannot copy or install the template. See template recovery. |
Unattended use#
A coordinator, a CI job or another agent can create, run, inspect and remove a
worktree with no terminal input. Give the global --non-interactive before
the command (treebox --non-interactive create ...), or set
TREEBOX_NON_INTERACTIVE=1 once for the whole job. It holds also with a
terminal attached; without a terminal on stdin treebox already behaves this
way. In unattended mode:
- Nothing opens a picker, asks a question, or waits on the terminal.
teardownwithout refs, or without--yes/--force, isNEEDS_CONFIRMATION(exit5). - git and ssh never prompt for credentials. treebox only tries its silent
credential paths (an ssh-agent, a credential helper, a
gh/glablogin, an HTTPS token), with git's terminal prompt off, ssh inBatchMode, and Git Credential Manager told not to open dialogs. A fetch that would need you to type something isFETCH_FAILED(exit4) at once. (In a terminal without--non-interactive, treebox lets git and ssh ask, likegit pull.) - Setup steps never read the terminal: their stdin is empty, so a step that
asks a question gets end-of-file and fails as
SETUP_FAILED. - A launch checks the agent before anything is provisioned: no harness binary
is
HARNESS_MISSING, no usable login isLOGIN_MISSING(both exit1) - an agent without a login would otherwise wait in its own login flow. Rundoctorfirst to check both. - Subscription logins stay the only auth: treebox never reads or passes an API key.
The clean flow#
Create the worktree with the task, replay its entry command, inspect it, and remove it. Every step is one command with a classified result:
export TREEBOX_NON_INTERACTIVE=1
treebox doctor --json > /dev/null # exit 1: the agent cannot run here
out=$(treebox create fix-login --json -- -p "Fix the login race and commit")
eval "$(jq -r '.entry_command | @sh' <<<"$out")" # runs the agent; $? is its exit code
treebox list --json | jq '.worktrees[] | select(.name == "fix-login")'
treebox teardown fix-login --yes --json # exit 5 if the agent left uncommitted work
create --json provisions without launching and puts the agent args in
entry_command; check its warnings (for example a skipped .env copy).
The entry command is self-contained, so it runs the agent in the worktree from
any directory, and its exit status is the agent's. list --json shows the
worktree's health and setup. teardown --yes confirms the removal but
keeps the uncommitted-changes check: work the agent did not commit is
DIRTY_WORKTREE (exit 5) and stays on disk, and --force removes it on
purpose.
To let treebox run the agent itself, launch directly:
treebox create fix-login -- -p "Fix the login race and commit" (or
treebox enter fix-login -- ... for an existing worktree). Then treebox
handles cancellation for you, and names the agent's exit
code as described below.
Agent exit codes#
When treebox launches the agent, it exits with the agent's exit code, so
an agent exit of 1 to 5 would look like a treebox failure. The two are
told apart by the last line on stderr:
- A treebox failure always prints its classified error (code, message and
hint; one JSON object under
--json) and never the line below. - When the agent exits non-zero, treebox prints one final line on stderr with this fixed prefix, then exits with the same code:
treebox: agent exited with status 3
An agent killed by a signal shows the shell-style status and the signal:
treebox: agent exited with status 137 (killed by SIGKILL). An agent exit
of 0 prints no line.
The prefix treebox: agent exited with status is a contract: match the last
stderr line against ^treebox: agent exited with status ([0-9]+).
Cancellation#
A cancel signal sent to the treebox process while it runs the agent stops the agent, in both runners:
| Signal to treebox | Host isolation | Docker isolation | treebox exits |
|---|---|---|---|
SIGINT |
Passed on to the agent process. | Passed on to the agent's process group inside the container. | 130 |
SIGTERM |
Same. | Same. | 143 |
SIGHUP |
Same. | Same. | 129 |
If the agent is still running 10 seconds after the signal, treebox kills it
(SIGKILL). treebox returns only when the agent is gone, and then fails with
CANCELLED (exit 128 + signal, never the agent exit line). The worktree is
kept; for docker the container keeps running, so the next
treebox enter <name> starts the agent again at once. (Stopping the local
docker exec client alone would leave the agent running in the container:
treebox signals the agent itself.)
This covers the agent session that treebox launches. A replayed
entry_command is a plain process that treebox does not supervise: on the
host, a signal to it reaches the agent directly; for docker, stopping the
docker exec client does not stop the agent in the container, so launch
through treebox when you need defined cancellation. A signal while treebox
is still provisioning stops treebox: SIGINT interrupts the step, the setup
result stays pending, and the next enter finishes it.