Are you still only working on one task at a time? Are you manually juggling between a few clones of the same repo?
Or... are you starting a new worktree for every agent session, losing all your installed dependencies and build cache each time, and wondering why your agents are slow?
Treehouse helps you manage a pool of reusable, isolated worktrees so each of your agents gets its own environment instantly — no cloning, no conflicts, no coordination overhead.
- Instant isolation —
treehouseputs you into a clean worktree with zero hassel. - Reusable worktrees — worktrees are preserved in a pool when you're done, with dependencies and build cache intact, ready for the next agent.
- Conflict-free — automatic detection of in-use worktrees and your agents never step on each other's toes.
$ cd myproject # start in your repo as usual
$ treehouse # get a worktree and drop into a subshell
🌳 Entered worktree at ~/.treehouse/myproject-a1b2c3/1/myproject. Type 'exit' to return.
# You're now in an isolated worktree.
# Run your AI agent, make changes, do whatever you need.
$ exit # exit the subshell when you're done
🌳 Terminated lingering processes: opencode (pid 12345)
🌳 Worktree returned to pool.macOS / Linux
curl -fsSL https://kunchenguid.github.io/treehouse/install.sh | shWindows (PowerShell)
irm https://kunchenguid.github.io/treehouse/install.ps1 | iexNix
nix run github:kunchenguid/treehouseOr add to your flake inputs:
treehouse = {
url = "github:kunchenguid/treehouse";
inputs.nixpkgs.follows = "nixpkgs";
};Go
go install github.com/kunchenguid/treehouse@latestFrom source
git clone https://github.com/kunchenguid/treehouse.git
cd treehouse
make installTreehouse manages a pool of git worktrees per repository, stored under the configured treehouse root.
The default treehouse root is ~/.treehouse/.
You can instead keep the pool inside the project with --root ., so it lives next to the code and is removed with the project.
treehouse
│
▼
Find repo root
│
▼
git fetch origin
│
▼
┌──────────────────────────────────────────────────────┐
│ Scan pool for a safely reusable worktree │
│ (idle, unleased, clean, and HEAD merged into the │
│ exact reset target; skip if safety is unprovable) │
└──────────┬───────────────────────────────────────────┘
│
┌────┴────┐
│ Found? │
└────┬────┘
yes/ \no
/ \
▼ ▼
Reset to Create new worktree
latest (detached HEAD at
default latest default
branch branch)
& add to pool
\ /
\ /
▼
Spawn subshell in worktree
(agent works here)
│
▼
exit subshell
│
▼
Terminate lingering worktree
processes and verify none remain
│
▼
Reset worktree & return to pool
(ready for next agent)
- Detached HEAD — worktrees use detached HEAD mode, reset to whichever of the local or remote default branch is further ahead, avoiding branch name conflicts entirely.
- No daemon - all operations are inline CLI commands. Pool state is a small on-disk file, written under a lock by each command.
- In-use detection — treehouse scans running processes and short-lived owner reservations to determine which worktrees are in-use. Reservations are persisted only while
get,destroy, andprunelifecycle work is running. - Durable leases -
treehouse get --leasereserves a worktree as a persistent home without keeping a process inside it. Each acquisition gets an immutable random lease identity, and the lease is recorded in treehouse's own state. The worktree is never handed out by a latergetand never removed bypruneuntil you release it withtreehouse return. Unlike process-based in-use detection, a lease survives with zero processes running inside the worktree. - State recovery - treehouse writes pool state atomically via a temp file and replacement.
If an existing state file is empty or truncated, treehouse warns, rebuilds entries from worktrees still on disk, and marks those entries leased until you verify them with
treehouse status. - Dirty detection - treehouse treats tracked changes and untracked files as dirty, even when repository config hides untracked files from normal
git statusoutput. - Safe pruning - By default,
treehouse pruneremoves only idle managed worktrees whose HEAD is already merged into the default branch and whose working tree is clean.treehouse prune --allapplies the same safety checks across every managed pool under the user-level treehouse root. Backing-repository-missing orphans are reported by default;--prune-orphansincludes them as unverified prune candidates, and--yesis required before deletion. It is a dry run unless you pass--yes. - Self-healing get -
treehouse getprunes stale git worktree bookkeeping (e.g. left behind by a crashed or forcibly removed worktree) before adding a new worktree, so a prunable registration never wedges the pool with a "missing but already registered worktree" error.
| Command | Description |
|---|---|
treehouse |
Get a worktree and open a subshell (alias for get) |
treehouse get |
Acquire a worktree from the pool |
treehouse get --lease |
Durably lease a worktree without a subshell; print its path |
treehouse enter <name> |
Open a subshell in an existing worktree by name (the number from status), even if it is in use; pool state is left untouched |
treehouse status |
Show pool status (highlights leased and current worktrees) |
treehouse return [path] |
Release any lease and return a worktree only after verifying foreign processes stopped |
treehouse prune |
Dry-run removal of stale idle worktrees in the current repo pool |
treehouse prune --all |
Dry-run removal of stale idle worktrees across every managed pool |
treehouse destroy <path> |
Dry-run removal of one worktree (safe by default; --yes to execute) |
treehouse destroy <pool> --all |
Dry-run removal of every disposable worktree in that pool |
treehouse init |
Create a default treehouse.toml config file |
treehouse update |
Update treehouse to the latest version |
| Command | Flag | Description |
|---|---|---|
get |
--lease |
Durably lease the worktree without opening a subshell; print only its path to stdout |
get |
--lease-holder |
Optional label recorded as the lease holder (defaults to $TREEHOUSE_LEASE_HOLDER) |
get |
--json |
Print path, lease_id, lease_holder, and leased_at as JSON (requires --lease) |
enter |
--print-path |
Print only the worktree's absolute path to stdout instead of opening a subshell (for cd "$(treehouse enter --print-path 1)") |
status |
--json |
Print worktree status and lease metadata as JSON |
return |
--force |
Clean, reset, and return without prompting |
return |
--if-lease-id |
Return only if the current lease has the expected per-acquisition identity |
return |
--if-lease-holder |
Return only if the current lease has the expected holder |
prune |
--yes |
Delete listed prune candidates instead of doing a dry run |
prune |
--all |
Sweep every managed pool under the user-level treehouse root |
prune |
--global |
Alias for --all |
prune |
--prune-orphans |
Include backing-repository-missing orphans in prune candidates |
prune |
--verbose, -v |
Show detailed skip diagnostics |
destroy |
--all |
Remove all worktrees in the named pool (requires a pool path) |
destroy |
--yes |
Execute the removal instead of doing a dry run |
destroy |
--include-unlanded |
Also remove dirty, unmerged, or unverified worktrees (irreversible data loss) |
destroy |
--include-in-use |
Also remove worktrees with a running process or owner reservation (processes are terminated cleanly first) |
destroy |
--include-leased |
Also remove a leased worktree; only when the exact path is named, never via --all |
treehouse get normally opens an interactive subshell whose lifetime is the hold: when the shell exits, the worktree returns to the pool.
That is awkward for callers that need a worktree to persist as a permanent home with no long-lived process inside it.
treehouse get --lease is the non-interactive, durable alternative:
path=$(treehouse get --lease)
# $path is the leased worktree's absolute path; all banners went to stderr.It acquires a worktree exactly like get, but instead of opening a subshell it marks the worktree leased in treehouse's persistent state. By default it prints only the worktree's absolute path to stdout; --json prints the lease allocation instead. Every human-facing message goes to stderr, so either output mode stays clean.
A leased worktree is never handed out by a later get and never removed by prune, regardless of whether any process runs inside it, until the lease is explicitly released.
A bulk treehouse destroy <pool> --all never removes it either; only naming its exact path with treehouse destroy <path> --include-leased --yes will.
Pass --lease-holder <label> (or set $TREEHOUSE_LEASE_HOLDER) to record who holds the lease; treehouse status then shows it next to the leased state.
Every acquisition receives a new random lease_id, including reacquiring the same path with the same holder. Automation can request a stable machine-readable allocation:
treehouse get --lease --lease-holder automation-A --json
# {"path":"...","lease_id":"...","lease_holder":"automation-A","leased_at":"..."}Callers that already fetched the required refs can avoid another network operation with --no-fetch:
git fetch origin main refs/pull/123/head
treehouse get --lease --no-fetch --jsonWith --no-fetch, Treehouse resets or creates the worktree from existing local refs and never contacts origin. The caller is responsible for ensuring those refs and objects are current.
treehouse status --json returns an array with name, path, status, flavor, lease_id, lease_holder, leased_at, and processes. flavor is the backend the worktree's own marker identifies ("git" or "jj") and is omitted when no marker is found. Non-leased entries use empty lease strings and a null timestamp. State files written before lease identities remain readable; their existing leases have an empty lease_id until released and acquired again.
Release a lease with treehouse return <path>, which terminates lingering processes and verifies that no foreign process remains before it resets the worktree, clears the lease, and returns the worktree to the pool.
If process termination or that verification fails, the command exits nonzero and leaves the worktree and lease in place instead of recycling a slot that may still be in use.
When you pass an explicit path, treehouse return can run from outside the repository because it resolves the managed pool from that worktree path.
For retry-safe automation, condition the return on the identity from allocation or status:
treehouse return --force \
--if-lease-id "$lease_id" \
--if-lease-holder "$lease_holder" \
"$path"Treehouse compares supplied conditions while holding the pool state lock. A missing lease or mismatch exits nonzero before process termination, worktree reset, or state clearing. The same lock fences a matching return through the final clear, so the identity succeeds once and cannot release a later acquisition of the same path. --if-lease-holder is optional; use --if-lease-id for ABA protection when a holder may be reused.
For backward compatibility, treehouse return <path> without either condition keeps its original unconditional path-only behavior. Existing path-only scripts and treehouse get --lease stdout are unchanged.
Treehouse writes treehouse-state.json atomically, so a crash mid-write should leave the previous state file intact.
If an existing state file is empty or truncated, commands do not fail just because the JSON cannot be parsed.
They print a warning, rebuild the pool entries from worktree directories still on disk, and mark every recovered entry as leased because treehouse cannot know whether it was idle, in-use, or durably leased.
Run treehouse status to inspect recovered entries.
After verifying a worktree is safe to reuse, run treehouse return <path> to clear the safety lease.
To delete one instead, name its exact path with treehouse destroy <path> --include-leased --yes.
Bulk destroy --all and prune leave recovered entries alone.
treehouse prune is a dry run by default.
By default, it lists stale idle managed worktrees that would be deleted and shows the reclaimable disk space.
Pass treehouse prune --yes to delete those worktrees.
By default, prune only inspects the current repository's pool and must be run inside a repository.
Pass treehouse prune --all or treehouse prune --global to inspect every managed pool under the user-level treehouse root from any directory.
Global prune reads the user-level config and hooks, derives each worktree's owning repository from version-control metadata, then fetches and checks merge safety against that repository.
Without --prune-orphans, pass treehouse prune --all --yes to delete only the globally safe stale candidates.
Prune ignores worktrees that are currently in use, leased, or reserved by another lifecycle operation.
It skips idle worktrees that are unsafe to remove and prints the skip reason, such as uncommitted tracked or untracked changes, or a HEAD commit that is not merged into the default branch.
Skip output is grouped by reason so large global sweeps stay scannable.
When origin exists, prune fetches it and proves each HEAD against the current remote default branch tracking ref.
Without origin, prune uses the local default branch ref.
If origin cannot be reached, prune reports origin unreachable (cannot verify) and leaves the worktree untouched, even when --prune-orphans is set.
If a linked worktree points at a missing backing repository, prune reports orphaned (backing repository missing).
Plain treehouse prune and treehouse prune --all never delete those orphans.
Pass --prune-orphans to include true backing-repository-missing orphans in the dry run, then add --yes to delete them.
Treehouse cannot verify orphan contents after the backing version-control metadata is gone, so each orphan candidate is marked content could not be verified.
Use --verbose to show the underlying version-control diagnostic details for skipped worktrees.
treehouse destroy is the deliberate tool for removing a worktree even though it still has unlanded work, but it is safe by default and holds itself to the same bar as prune.
Targets are narrow and explicit:
treehouse destroy <worktree-path>targets exactly one worktree.treehouse destroy <pool-path> --alltargets worktrees in THAT pool only. The pool path can be the pool directory, a worktree inside it, or the repository (.works from inside a repo).
There is no cross-pool or global destroy: --all without a pool path is an error, so a stray command can never reach beyond the pool you named.
Destroy is a dry run by default.
It prints a risk-revealing preview - one or more status labels ([disposable], [leased], [in-use:<pid>], [unmerged], [dirty], [unverified], or a comma-separated combination such as [leased,dirty]), the path, and the size of each target - and removes nothing.
Pass --yes to execute.
It never prints a blind "all worktrees destroyed"; the summary always reports exactly what was destroyed and what was skipped.
A bare treehouse destroy <pool> --all --yes removes only the genuinely disposable set (merged, clean, idle, unleased - the same set prune would take) and SKIPS everything else, telling you which flag would include it.
Each risky class is its own opt-in, so removing risky worktrees can never be a reflexive --yes:
--include-unlandedalso removes worktrees with uncommitted changes, a HEAD not merged into the default branch, or contents treehouse cannot verify, such as a missing backing repository (irreversible data loss).--include-in-usealso removes worktrees with a running process or owner reservation; their processes are terminated cleanly first and their pids are shown in the preview.--include-leasedalso removes a leased worktree, but only when you name the exact worktree path. Leased worktrees are NEVER removed by--all; combining--include-leasedwith--allis rejected.
A single named worktree that is skipped for lack of a flag makes the command exit non-zero, so scripts notice that nothing happened.
Bulk --all skips are normal and exit zero; inspect the summary to see what remains.
The old blunt treehouse destroy --force flag has been removed.
It overrode every protection at once - in-use, unmerged, dirty, and leased - which is what made it dangerous.
Replace it with the specific --include-* flag(s) for the risk you actually intend to override, plus --yes:
| Old | New |
|---|---|
treehouse destroy <path> --force |
treehouse destroy <path> --yes (add --include-unlanded / --include-in-use / --include-leased as needed) |
treehouse destroy --all --force |
treehouse destroy <pool> --all --yes (add --include-unlanded for dirty, unmerged, or unverified targets, and --include-in-use for in-use targets; leased homes are never included) |
Create a repo config file with treehouse init, or add one manually:
Repo-level: treehouse.toml in the repository root
User-level: ~/.config/treehouse/config.toml
# Maximum number of worktrees in the pool
max_trees = 16
# Optional worktree root directory.
# Empty uses $HOME/.treehouse.
# Relative paths are resolved from the repo root for repo-scoped commands.
# Use "." to keep the pool inside the project (see "In-project storage" below).
# Use an absolute user-level root for treehouse prune --all.
# root = "$HOME/worktrees"
# Optional version-control backend. Git is the default everywhere; set "jj"
# to opt in to the experimental Jujutsu backend
# (see "Version-control backend" below).
# vcs = "jj"The repo-level config takes precedence for repo-safe settings.
treehouse prune --all can run without a repository, so it uses only the user-level config and does not read per-repo treehouse.toml files while sweeping.
If no config is found, the default pool size is 16.
Treehouse works in git and Jujutsu (jj) repositories. The jj backend is experimental: it is newer than the git backend and has seen far less production use, so treat it accordingly and report issues. In a jj repository, pooled worktrees are jj workspaces instead of git worktrees; the pool, lease, and safety machinery is identical.
Git is the default backend everywhere, including in colocated repositories (both .jj and .git) and .jj-only repositories.
Opt in to the jj backend with vcs = "jj", resolved in this precedence (highest first): the TREEHOUSE_VCS environment variable, the repo-level treehouse.toml, the user-level ~/.config/treehouse/config.toml.
The jj opt-in only applies where a .jj directory actually exists; in a plain git repository it is silently ignored and git is used, so a shell-wide TREEHOUSE_VCS=jj never breaks git-only repositories.
A vcs value other than "git" or "jj" (e.g. "Jujutsu") is ignored the same way, but warns once on stderr naming the value and where it came from, so a typo keeps commands working without silently leaving you on the wrong backend.
Pooled jj workspaces inherit the opt-in from their main repository root, so an untracked treehouse.toml there is enough.
The backend is resolved on every command, and existing pool slots keep the flavor they were created with: changing the opt-in does not convert worktrees already in the pool.
destroy and prune handle each slot by its own flavor (its .git or .jj marker), so a git worktree is still cleanly deregistered from git even after opting the repository into jj, and vice versa.
A slot whose marker is missing entirely (a damaged slot) is never reused, reset, or detached; treehouse status reports it as damaged, treehouse return only clears its lease, prune reports it as unverifiable, and treehouse destroy <path> --include-unlanded removes it.
treehouse get is flavor-aware too: it only reuses slots matching the backend the repository currently selects, and creates new slots with that backend, so a caller who opted in to jj is never handed a git worktree (or vice versa).
Old-flavor slots stay in the pool untouched — treehouse status marks them and they count toward max_trees — until you migrate them: treehouse destroy the old slots and re-acquire with treehouse get.
jj-backend notes:
- Pooled worktrees are jj workspaces and are not colocated: they contain
.jjbut no.git, so run jj commands (not git) inside them. - A worktree is considered dirty when its working-copy commit
@is non-empty or has a description. - Resets abandon only the working-copy commit and are recoverable with
jj op restore. - Merge detection uses ancestry; squash-merged work is treated as unmerged, so lifecycle commands err on the side of keeping it.
- The default branch resolves to the
main/master/trunkbookmark, preferring origin. - A pooled jj workspace whose backing repository was deleted is classified as an orphan just like a git worktree:
prunereports it, andprune --prune-orphans --yesreclaims it.
The worktree root can also be set without a config file, and the resolved value follows this precedence (highest first):
- The
--rootflag (e.g.treehouse get --root .) - The
TREEHOUSE_ROOTenvironment variable rootin the repo-leveltreehouse.tomlrootin the user-level~/.config/treehouse/config.toml- The default,
~/.treehouse
A relative value (including .) is resolved from the repo root, exactly like a relative root in config; treehouse is always appended, so --root . places the pool at <repo>/.treehouse/.
By default the pool lives in the global ~/.treehouse store. Set the root to . to keep it inside the project instead:
treehouse get --root . # one-off
export TREEHOUSE_ROOT=. # for a shell sessionor commit it for the whole repo in treehouse.toml:
root = "."This is opt-in; the default global store is unchanged. In-project mode:
- Places the pool at
<repo>/.treehouse/, so worktrees sit next to the code and are removed with the project (rm -rf <repo>leaves no global orphan). - Git-ignores the pool directory automatically, so it stays out of
git add. - Is not reached by
treehouse prune --all, which only sweeps the global root; in-project pools are removed with the project instead.
You can run commands automatically at worktree lifecycle points by adding a [hooks] section to the user-level config at ~/.config/treehouse/config.toml.
Hooks in repo-level treehouse.toml are ignored for safety.
treehouse destroy always reads pre_destroy from the user-level config because it can target a pool by path.
[hooks]
post_create = ["./scripts/setup-venv.sh"]
pre_destroy = ["./scripts/teardown.sh"]post_createruns after a worktree is provisioned or reset and right beforetreehouse gethands it to you. Fortreehouse get --lease, stdout frompost_createis routed to stderr so stdout remains the leased path.pre_destroyruns before a worktree is removed bytreehouse destroy <path> --yes,treehouse destroy <pool> --all --yes, or prune deletion commands such astreehouse prune --yesandtreehouse prune --prune-orphans --yes.
Commands in each list run sequentially in the worktree directory, via the OS shell (/bin/sh -c on Linux/macOS, %COMSPEC% /c on Windows).
If a command exits non-zero, treehouse logs the command, exit code, and stderr, then continues with the remaining commands.
A failing hook does not fail the overall get, destroy, or prune operation.
make build # Build the binary
make test # Run tests
make lint # Run gofmt + go vet
make dist # Cross-compile for all platforms
make install # Install to $GOPATH/bin or /usr/local/bin
make clean # Remove build artifacts