liminaltrees(1) General Commands Manual liminaltrees(1)

Name

liminaltrees — every git worktree on your machine, at once

Synopsis

liminaltrees [--root path]
liminaltrees scan [--json] [--root path]
liminaltrees gc [--dry-run] [--yes] [--root path]
liminaltrees new branch [--subject text]
liminaltrees config [set-root path]
liminaltrees init zsh|bash|fish
liminaltrees install-hook [--uninstall]
liminaltrees ui [--root path]

--root takes a repository or a directory of them, and belongs after the subcommand. Every run prints the scope it resolved before it prints anything else.

Install

$ curl -fsSL https://lt.different.productions/install.sh | sh

macOS 15 or later. Universal binary, ~1 MB, no runtime dependencies. Nothing to hand-edit. The script checks its own SHA-256 before unpacking, and you can read it first — it is served as plain text.

Description

git worktree list shows you one repository at a time, and tells you nothing about what any of the worktrees are for. So they accumulate — the ones you made, the ones your agents made and named after a hash, and the ones whose contents your OS quietly deleted without telling git.

liminaltrees finds all of them, works out what each one is, and removes the ones whose work has already landed. Anything holding work that exists nowhere else gets pushed somewhere safe first, and is only removed once that push is confirmed.

It sees through a squash merge, which git branch --merged cannot: the squashed commit has different parents and a different hash, so the original branch reads as unmerged forever. That is why worktrees for work you shipped weeks ago are still sitting there.

Setup

The command above is enough for liminaltrees, scan and gc. Two optional commands wire up the rest. Each edits one file, and each undoes itself.

liminaltrees config set-root ~/where/your/repos/live
Only if your checkouts are somewhere it does not already look — it tries ~/Developer, ~/Projects, ~/code, ~/src, ~/dev, and the directory holding whatever repository you are standing in. liminaltrees config show tells you what it picked and why.
eval "$(liminaltrees init zsh)"
Add to ~/.zshrc. Defines wt <branch>, which creates a worktree and drops you inside it. It has to be a shell function because no process can change its parent shell's directory. bash and fish too.
liminaltrees install-hook
Records where agent worktrees came from at the moment they are created, so an abandoned one is still identifiable after its branch and pull request are gone. Adds one Claude Code SessionStart hook and leaves the rest of the file alone. --uninstall restores it exactly.

Commands

liminaltrees
Opens the dashboard. Piped, prints a table instead, so | grep still works.
liminaltrees gc
Shows you exactly what it would do, grouped by what it intends. Runs only if you type y.
liminaltrees gc --dry-run
Shows the plan and stops. Never prompts.
liminaltrees scan --json
For the agents that made this mess in the first place. Stable tokens, not prose.
liminaltrees new branch
Creates a worktree and records what it is, before anyone has to guess later. Prints only the path, so cd "$(liminaltrees new my-branch)" works.
liminaltrees config show
Where it will look, and whether that came from your config or a guess.

How it decides

Checks run in order. The first one that matches decides, so the order is the whole safety policy.

  1. Detached, on a commit no upstream branch hasheld — nothing to rescue onto, and its registration holds the only ref
  2. Registered, but the directory is goneremove — bookkeeping only; the branch ref outlives it
  3. Anything wrote to it in the last hourheld — something is working in there
  4. Uncommitted filesrescue — those bytes exist in one place on earth
  5. Commits on no remote, and no patch upstreamrescue — still unique
  6. Its pull request merged or closedremove — this is the one that catches squash merges
  7. Every commit's patch is already on the default branchremove — it landed some other way
  8. Clean, pushed, unmergedyour call — safe to delete, but still somebody's plan

It will never

touch a worktree something is writing to
Anything written in the last hour is off limits, and that check outranks the rescue rules — pushing a half-finished state out from under a live agent run is worse than leaving the disk alone.
delete work that exists nowhere else
Uncommitted, unpushed, or reachable from no remote: it gets committed, pushed to a fresh wip/ branch, and confirmed on the remote. Only then is the worktree removed. A rescue that fails cancels that removal and nothing else.
guess
If it cannot reach the forge and cannot prove a branch landed, it says so and leaves the worktree alone. Not knowing is a result, not a reason to delete.
act without showing you first
Every removal is listed, with the single fact that decided it, before anything runs.

Files

~/Developer/.worktrees/project/branch The ones you made on purpose.
repo/.claude/worktrees/agent-* Made by a harness run, named after a hash, never cleaned up.
/private/tmp/claude-uid/session/scratchpad/ The OS reclaims the contents. Git keeps the registration forever.
~/.local/bin/liminaltrees Where the installer puts it.

Guarantee

A worktree is removed only when every commit in it is reachable from origin and the working tree is clean. After any run, git fsck reaches everything it reached before.

That is the point of the whole design: a plan you can approve without reading it closely. A cleanup tool you have to read closely every time is a cleanup tool you stop running.

Bugs

Rescue has no automated test coverage — exercising it means creating remote branches. Every classification rule is tested; the push-and-verify sequence has been read, not run.

In 0.1.0, --root typed after a subcommand was silently ignored and the run covered the whole machine instead. If you are on 0.1.0, do not trust a scoped gc. Fixed in 0.2.0, which prints the scope it resolved above everything else.

Removing a worktree also removes its DerivedData, which git cannot restore — it is rebuilt by building again. The plan lists every build directory by name before anything runs, and reports that figure separately from the git one.

macOS only. It shells out to stat -f, which is the BSD spelling, and DerivedData is an Xcode idea.

See also

git-worktree(1), git-cherry(1), and the twenty-three checkouts you forgot about

Different Productions Version 0.2.0 liminaltrees(1)