CLI reference
tama worktrees remove
Removes every linked git worktree under the roots you name, previewing the whole pass unless --apply is given, and refusing the entire pass before deleting anything when a selected worktree is dirty or locked.
#Invocation
tama worktrees remove [--root <PATH>]... [--except <PATH>]... [--apply] [--force] [--json]
#Inputs and options
| Option | Contract |
|---|---|
--root <PATH> | Repeatable, and required at least once. Each value is a directory under which Tama looks for the repositories that own worktrees. There is deliberately no default root |
--except <PATH> | Repeatable, and optional. Each value is the path of one linked worktree to leave completely alone. A value that matches no worktree this pass would otherwise consider is a usage error |
--apply | Performs the removal. Without it the command previews and mutates nothing |
--force | Proceeds past dirty and locked worktrees and discards their uncommitted changes. Without it either condition refuses the pass |
--json | Prints the exact document below instead of the text report, in both preview and applied mode |
There is no argument that selects one worktree for removal: the pass is
every linked worktree found under the roots, minus the ones you name with
--except. Narrow it by naming narrower roots, and see what it would
touch with tama worktrees list or with this
command's own preview.
#Keeping one worktree with --except
--except is repeatable and takes the path of one linked worktree. An
excepted worktree is left completely alone: it is not removed, it is not
counted as removable, and it never produces a refusal. It appears in the
document under the excepted array, so an applied pass reports which
worktrees it deliberately did not touch.
It exists because the refusal is the whole pass, which is the right contract and also means one worktree in use blocks every other one. On the machine this option was written for, 39 worktrees could not be removed because a fortieth was being written by a live agent session, and root selection could not separate them because a root selects owning repositories — one repository owned both the worktree in use and many of the others. Naming that one path is how the removal proceeds without destroying another session's in-flight work.
An --except value that matches no worktree this pass would otherwise
consider is refused rather than ignored, so a mistyped path can never
silently remove the thing it meant to keep. Path comparison is canonicalised,
the same way the code already compares a main worktree path.
tama worktrees list has no
--except: it removes nothing, so it has nothing to exempt, and its
document carries no excepted array. This is a remove option
only.
#Discovery and removal mechanics
tama_violations::repo_discovery's discover_repos(root) finds the
owning repositories under each root; git itself is then the authority on
worktrees, through git -C <repo> worktree list --porcelain. The
repository's own main worktree is never reported and never removed. A
worktree is dirty when git -C <worktree> status --porcelain prints
anything.
Each removal is git's own operation, never a directory delete:
git -C <repo> worktree remove [--force] <path>
git -C <repo> worktree prune
--force is passed through to git worktree remove only when you
passed it. The prune runs after the removals so the owning repository's
.git/worktrees administrative entries do not outlive the checkouts.
#Output document
Under --json, exactly:
{"schemaVersion":1,"roots":[String],"repositories":[{"repository":String,"worktrees":[{"path":String,"branch":String|null,"head":String,"dirty":Boolean,"locked":Boolean,"prunable":Boolean}]}],"worktreeCount":Number,"unreadable":[String],"unreadableDirectories":[{"path":String,"reason":String}],"excepted":[String],"applied":Boolean,"removed":[String],"refused":[{"path":String,"reason":"uncommitted-changes"|"locked"}]}
It is the list document plus four fields, with
excepted immediately after unreadableDirectories.
excepted holds the canonicalised path of every worktree you named with
--except, so the document states what the pass deliberately left alone;
it is [] when you named none. Those worktrees appear nowhere in
removed and nowhere in refused, in a preview and in an applied
pass alike.
applied is true only for an --apply pass that actually
ran: it is false for every preview, and false for an
--apply pass that was refused. removed holds the paths git
actually removed, so a preview and a refused pass both report
"removed":[]. refused names every worktree that blocks
the pass — each with the reason "uncommitted-changes" or
"locked" — and it is populated in a preview too, so a bare
remove shows exactly what --apply would refuse. --force
empties refused in both modes, because nothing blocks a forced pass.
unreadableDirectories is the list
report of what the walk could not read, and it ends this pass: an
all-or-nothing removal cannot be all-or-nothing over a tree it could not see,
so a pass carrying one of these answers "applied":false,
"removed":[], prints could not read <path>: <reason>
followed by this pass could not read N directories under the roots, so it
cannot say what they hold; nothing was removed, and exits 1. This
holds for a preview too, because neither answer covers the tree the operator
named.
The same document is the answer of tama request worktrees/remove with body
{"roots":[String],"except":[String],"apply":Boolean,"force":Boolean}
(tama request), where except is
optional and defaults to empty. An empty roots array answers status
400 with the missing-root sentence below; a refused pass answers status
200 with the refusal document and the request exits 0, where
the CLI exits 1.
#State effect
A bare remove changes nothing: it is a preview, and it exits
0 even when its refused array is non-empty, because a preview
has nothing to refuse yet. Only remove --apply deletes, and then only
through git worktree remove followed by git worktree prune.
The refusal check runs across the whole selection before the first removal,
so an applied pass either removes every worktree it selected or removes none
of them — Tama does not delete the first three worktrees and then discover
the fourth is dirty.
#Refusal boundary
Passing no --root is a usage error, exit 2:
tama worktrees needs at least one --root; a machine-wide default would walk directories Tama does not own.
An --except value that matches no worktree this pass would otherwise
consider is a usage error, exit 2:
tama worktrees does not know the worktree <path>; --except takes the path of a worktree this pass would otherwise remove.
--apply without --force refuses the whole pass before deleting
anything if any selected worktree carries uncommitted changes or git reports
it locked, and names every one of them, one sentence per worktree, exit
1:
Refusing to remove <path>: it carries uncommitted changes. Commit them in that worktree, or pass --force to discard them.
Refusing to remove <path>: git reports it locked. Unlock it with git worktree unlock, or pass --force.
Both sentences name the repair you probably want first — commit in that
worktree, or git worktree unlock — and the switch that overrides it.
--force proceeds and discards those changes; that is the whole meaning of
the flag, and it is why it is not the default.
A refused --apply pass reports "applied":false and
"removed":[] with every blocker in refused: the refusal is
the whole pass, not the worktrees that happened to be clean.
A failed git worktree remove or git worktree prune is an operational
failure, exit 1. Otherwise the command exits 0: every preview
does, and so does an applied pass that removed everything it selected,
including the pass that found nothing to remove.
Return to the worktrees command group or the complete CLI tree.