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** — `treehouse` puts 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.
## Quick Start
```sh
$ 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.
```
## Install
**macOS / Linux**
```sh
curl -fsSL https://kunchenguid.github.io/treehouse/install.sh | sh
```
**Windows (PowerShell)**
```powershell
irm https://kunchenguid.github.io/treehouse/install.ps1 | iex
```
**Nix**
```sh
nix run github:kunchenguid/treehouse
```
Or add to your flake inputs:
```nix
treehouse = {
url = "github:kunchenguid/treehouse";
inputs.nixpkgs.follows = "nixpkgs";
};
```
**Go**
```sh
go install github.com/kunchenguid/treehouse@latest
```
**From source**
```sh
git clone https://github.com/kunchenguid/treehouse.git
cd treehouse
make install
```
## How It Works
Treehouse manages a **pool of git worktrees** per repository, stored under the configured treehouse root.
The default treehouse root is `~/.treehouse/`.
```
treehouse
│
▼
Find repo root
│
▼
git fetch origin
│
▼
┌───────────────────────────────────────┐
│ Scan pool for available worktree │
│ (not leased, not in-use, not dirty) │
└──────────┬────────────────────────────┘
│
┌────┴────┐
│ 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, 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. No background processes, no state to get corrupted.
- **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`, and `prune` lifecycle work is running.
- **Durable leases** — `treehouse get --lease` reserves a worktree as a persistent home without keeping a process inside it. The lease is recorded in treehouse's own state, so the worktree is never handed out by a later `get` and never removed by `prune` until you release it with `treehouse return`. Unlike process-based in-use detection, a lease survives with zero processes running inside the worktree.
- **Dirty detection** - treehouse treats tracked changes and untracked files as dirty, even when repository config hides untracked files from normal `git status` output.
- **Safe pruning** - By default, `treehouse prune` removes only idle managed worktrees whose HEAD is already merged into the default branch and whose working tree is clean.
`treehouse prune --all` applies the same safety checks across every managed pool under the user-level treehouse root.
Backing-repository-missing orphans are reported by default; `--prune-orphans` includes them as unverified prune candidates, and `--yes` is required before deletion.
It is a dry run unless you pass `--yes`.
## CLI Reference
| 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 status` | Show pool status (highlights leased and current worktrees) |
| `treehouse return [path]` | Release any lease, terminate lingering worktree processes, and return it to the pool |
| `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 ` | Dry-run removal of one worktree (safe by default; `--yes` to execute) |
| `treehouse destroy --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 |
### Flags
| 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`) |
| `return` | `--force` | Clean, reset, and return without prompting |
| `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` |
### Leasing a worktree (no subshell)
`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:
```sh
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 and prints only the worktree's absolute path to stdout (every human-facing message goes to stderr, so command substitution 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 --all` never removes it either; only naming its exact path with `treehouse destroy --include-leased --yes` will.
Pass `--lease-holder