Skip to content

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)

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. The flattened form is the worktree's permanent identity: the directory (.treebox/worktrees/<name>), the lock key, and what enter / teardown address. 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.

--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/authfeature--auth) 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 adoption 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 the exact commands that would run; 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 died after registering the worktree but before setup completed, running the same create again finishes setup instead; a fully provisioned same-name worktree is still a conflict.

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 the exact 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 is also a real preflight: it enforces the same read-only preconditions as create and fails with the identical 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 - rather than printing a plan a real run would refuse. The one exception mirrors 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. Either way, nothing on disk or in git changes.

enter — come back to a worktree#

treebox enter fix-auth --harness claude
treebox enter fix-auth --harness codex -- --resume

enter (and teardown) take a ref: the worktree name, its current branch, or a unique substring of either — resolved live from git, so a branch the agent renamed five minutes ago still works. An ambiguous ref is a loud exit 2 listing the matches, never a guess. 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. 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 (treebox stores the hash in the worktree's private git dir, so it never shows up in git status); a re-sync preserves the recorded harness and template rather than stamping in the session's choice. If a previous run died before setup completed, enter finishes the setup even though the recorded hash matches, instead of launching into a half-built tree. Anything after -- is passed through to the agent. --cold forces a cache-bypassing re-sync.

Under docker isolation, enter preflights the Docker daemon 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. 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.

list — what exists, what's stale#

(treebox ls works too.)

$ treebox list

  NAME           BRANCH                 LAST COMMIT              AGE    DEPS       ENV
  ─────────────────────────────────────────────────────────────────────────────────────────
  fix-auth       fix/ci-caching         cache uv wheels in CI     2h    ● fresh    ● present
  brave-otter    ⚠ unnamed              initial provision         3d    ● stale    ● present

  2 worktrees · 1 unnamed · 1 stale
  ↳ unnamed: rename before push — 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 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.

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

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. For docker-sandboxed worktrees, --remove-volumes also removes treebox volumes and --skip-container leaves containers/images untouched. --json prints a structured record of what was removed (and never blocks on a prompt).

Container/image cleanup and local branch deletion are best-effort after the target set is chosen: if a container or image removal fails, treebox warns, still attempts every remaining cleanup step (volumes, sandbox files), removes the worktree, and reports container: "failed"; a failed volume removal alone warns and reports volumes_removed: false without marking the container failed; if Docker itself is unavailable, the worktree is still removed and the record reports container: "skipped" with volumes_removed: false, even under --remove-volumes; if branch deletion fails, it 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 is pure local git — dirty tree, ahead/behind, merged into your base, never-pushed — so it works on any remote (GitHub, GitLab, Bitbucket, plain git) and any auth. A freshly created worktree with no commits of its own reads ⚠ empty (safe to delete, but not to be mistaken for a landed branch) rather than ✓ merged, since a placeholder sitting exactly at your base trivially counts as "merged into" it. 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 — which is the only reliable way to spot a squash merge. Everything degrades cleanly: no CLI, an unknown host, or offline just falls back to the local badge.

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 or a non-TTY, pass refs explicitly.

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 treebox 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 --force 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 recorded isolation and template through git's own registration rather than the missing pointer, so a corrupt docker worktree is still torn down with the runner it was created with.

If a worktree's recorded isolation mode is unknown (corrupt or hand-edited state), teardown refuses it as a conflict rather than guessing how to drive its container — the same stance enter takes. Pass --skip-container to remove the tree and its branch anyway and clear any leftover container yourself.

doctor — is this host ready?#

treebox doctor                       # checks for host isolation
treebox doctor --isolation docker    # also checks the Docker daemon

Checks git, agent logins, .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. Hard failures (git, repo, or the selected isolation mode) exit 1 in both human and --json modes, 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, required-file status, and the config default (ls works too)
treebox template path node                 # print the resolved dir: cd "$(treebox template path node)"

template init <name> always yields a directory with the full required file set, so create --template <name> can't fail on a half-copied template; it refuses to clobber an existing template without --force. Then edit the Dockerfile and container.json and launch with treebox create <name> --isolation docker --template <name>. template list flags any template dir missing a required file before create does, 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 doesn't exist
4 auth — fetch or credential problem
5 conflict — name taken, dirty tree, or lock held

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:

$ treebox create fix-auth --json
{
  "schemaVersion": 1,
  "name": "fix-auth",
  "worktree_path": "/home/you/code/myapp/.treebox/worktrees/fix-auth",
  "branch": "treebox/fix-auth",
  "base": "main",
  "entry_command": ["sh", "-c", "cd /home/you/code/myapp/.treebox/worktrees/fix-auth && exec claude --dangerously-skip-permissions"],
  "created": true
}

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. Without --json, --print, or --dry-run, create and enter launch the agent and exit with the agent process's exit code. 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
create --dry-run --json schemaVersion, dry_run, name, worktree_path, branch, commands
list --json schemaVersion, worktrees
teardown --json schemaVersion, worktrees
doctor --json schemaVersion, ok, isolation, checks, advisories

list rows contain name, branch, unnamed, missing, last_commit, commit_epoch, path, base, isolation, harness, deps, and env. deps is fresh, stale, or unknown; env is present or absent. teardown records contain name, branch, worktree_path, removed, branch_deleted, container, and volumes_removed; 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. 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.

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.
INVALID_NAME 2 create NAME is not slash-separated lowercase slugs, or uses the reserved treebox/ prefix.
INVALID_BRANCH 2 create NAME or --checkout names an invalid git ref.
NOT_A_REPO 2 --repo is not a git repo.
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.
MISSING_DEPENDENCY 1 Required runner dependency is missing.
DOCKER_UNAVAILABLE 1 Docker is installed but the daemon is unavailable.
ERROR 1 Unclassified runtime, setup, or template failure.
SLUG_CONFLICT 5 The worktree name is already taken.
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.
DIRTY_WORKTREE 5 Explicit teardown target has uncommitted changes.
NEEDS_CONFIRMATION 5 Teardown would need an interactive choice or confirmation.
LOCK_HELD 5 Another treebox operation holds this worktree's lock.
UNKNOWN_ISOLATION 5 Recorded isolation mode is unknown (corrupt or hand-edited state).
ISOLATION_MISMATCH 5 Explicit --isolation disagrees with the recorded mode.
TEMPLATE_NOT_FOUND 3 template init --from / template path names a template that doesn't exist.
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.