Skip to main content

Module worktree

Module worktree 

Source
Expand description

Git-worktree helpers for the worker spawn and despawn paths: detecting a repo, reading a worktree’s branch, deciding whether a worktree is safe to remove, removing it, and reaping the branch behind it.

Synchronous throughout because each answer is one fast git subprocess and the callers are already on a tokio task.

Re-exports§

use std::collections::HashMap; 🔒
use std::io::Write; 🔒
use std::path::Path; 🔒
use std::path::PathBuf; 🔒
use std::process::Command; 🔒
use std::sync::OnceLock; 🔒
use super::git_command; 🔒

Structs§

AgentWorktree
A completed isolation: "worktree" subagent’s managed worktree, as the Agent tool reports it on completion. Produced only by completed_agent_worktree, which vouches for every field.
ListedWorktree 🔒
One git worktree list --porcelain entry.
RecordedWorktree 🔒
One record, as recorded_worktree reads it.
RepoBranches
What a repo knows about its branches, in two quantities that must not be confused for each other.
WorktreeLock 🔒
A worktree lock: the canonicalized path plus the pid the CLI stamps into its reason, when it carries one.

Enums§

AgentWorktreeReap
Outcome of reap_agent_worktree.
BranchReapOutcome
Outcome of reap_worktree_branch. Kept, KeptOnError and DeleteFailed each leave a branch behind and the caller warns differently for each; NotPresent warns about nothing.
WorktreeEnsure
What ensure_worker_worktree had to do, so a caller rolling back a refused spawn knows what it may delete: the worktree only when it was absent, the branch only when this call minted it.
WorktreeError
Errors from remove_worktree.
WorktreeRecord 🔒
What forge recorded about a worktree it owns, in the repo’s git common dir so the record moves with the repo.
WorktreeRegistration 🔒
Where the worktree at path stands with git’s registrations.

Constants§

WORKTREE_RECORDS 🔒

Functions§

agent_worktree_lock_is_live
Whether path’s lock, re-read now, belongs to a live process. The sweep re-runs this immediately before each removal so a lock taken after the listing still stops that reap. Unlocked or dead-owner trees answer false; unreadable answers true.
branch_ref_exists
Whether branch resolves at repo, as a local head or as any remote-tracking ref. A repo git cannot read answers true, so a caller cleaning up on absence never discards state it failed to inspect.
completed_agent_worktree
The agent worktree a completed Agent-tool call left behind, from the PostToolUse hook payload.
detached_unique_tip 🔒
The full sha of a detached worktree HEAD holding commits that only the worktree itself reaches, or None when removal cannot strand anything. An attached branch is judged after removal by reap_worktree_branch; a detached HEAD must be judged before.
discard_worker_worktree
Undo the work ensure_worker_worktree did for a spawn that was then refused, so the refusal strands neither worktree nor branch. The worktree goes only when ensure created it, the branch only when ensure minted it - an AttachedExistingBranch keeps the branch, which predates the call and may hold the worker’s only copy of its commits. Returns git’s complaint when something had to stay behind; the spawn is already refused, so a failure is a warning, not an error.
ensure_worker_worktree
Recreate the worker worktree at path when a despawn removed it, so a resumed worker can run with its worktree as cwd again - the transcript lives under the worktree’s storage key, so claude --resume cannot find it from any other directory. Anchored on the branch: attach the surviving worktree-<label> when despawn kept it, otherwise recreate the branch from origin/<default-branch> - the base claude’s own --worktree <label> uses under its default worktree.baseRef of fresh.
find_string_field 🔒
First string value under key in a pre-order walk. The walk is depth-capped at 6: the captured shape is flat and top-level, so a deeper field is treated as absent (surfaced by the caller’s parse-miss log, never guessed at). String content is never searched, so a result text quoting worktreePath: cannot aim the reap.
first_unresolved 🔒
True the first time this process reports root as unresolved, so the warning is one line per root rather than one per render-path call.
foreign_leftover_detail 🔒
Why the directory at path is not this repo’s leftover, when its .git entry says so - None when it is ours to delete.
git_common_dir 🔒
The repo’s git common dir, resolved and canonicalised. A project checked out as a linked worktree or a submodule answers with the dir its gitdir lives under, not <repo>/.git.
git_config_value 🔒
One git config --get value, None when unset or unreadable.
git_error 🔒
git’s stderr, or its exit status when it said nothing.
git_in_repo 🔒
Run git -C repo <args> with the ambient repo-location env scrubbed, returning stdout on exit 0.
is_git_repo
Check whether path is inside a git repository. Shells out to git rev-parse --git-dir. Exit 0 -> true; anything else -> false.
is_isolated_agent_call
Whether this hook payload names an isolation:“worktree” Agent call - the population whose completion either names a managed worktree or proves the CLI’s tool-response contract drifted.
listed_worktrees 🔒
The entries of a porcelain listing: one block each, a worktree <path> header followed by the branch line when the entry holds one.
live_foreign_git_dir 🔒
Why the .git link at path names a LIVE git dir that is not this repo’s, when it does; None when it names this repo’s common dir or the git dir is gone. A stale or absent target is not proof of foreign ownership on its own, so callers weigh it separately.
lock_pid_is_live 🔒
Whether a lock blocks the sweep: a parsable pid whose process is alive blocks, and so does one we cannot judge (no pid, or liveness unanswerable); only a provably dead pid releases the tree.
locked_worktrees 🔒
This repo’s worktrees git reports as locked, canonicalized, with the pid parsed from the reason when there is one. None when git cannot be asked - the caller sweeps as empty rather than judge without the lock facts.
moved_registration_names 🔒
Whether a listed worktree path that no longer resolves still names the worktree at ours. A repo moved or renamed leaves its registrations carrying the old absolute path, while the tail below the repo root - .claude/worktrees/<label> - is intact. The listing is this repo’s own, so a matching tail is this repo’s registration, not a foreign one.
obstacle_from_listing 🔒
The obstacle the listing describes: the branch held at another path, or what git has registered at path and will not reuse.
origin_base 🔒
The ref claude’s --worktree cuts a fresh branch from: origin/<default-branch> as the repo’s origin/HEAD names it.
parse_lock_pid 🔒
The pid in a CLI lock reason, claude agent agent-<hex> (pid 33497 start ...). None for a bare locked line or any foreign reason.
path_is_taken 🔒
Whether something at path is in the way of git worktree add, which takes a path that is absent or an empty directory and refuses anything else: a file, and any directory holding an entry, both exit 128 (“already exists”). A dotfile counts as an entry - measured.
process_is_live 🔒
Whether pid names a live process. Unanswerable counts as live: a liveness question we cannot ask never deletes a tree.
reap_agent_worktree
Remove a completed subagent’s managed worktree, then reap the conventional branch behind it. Refuses anything but a porcelain-clean tree: the measured leak is clean trees whose gitignored target/ the CLI’s auto-clean does not count, while uncommitted edits are a tree the CLI handed back deliberately. Only branch is ever aimed at - a branch the subagent checked out itself is left to git’s normal lifecycle, the same rule the worker despawn applies.
reap_worktree_branch
Delete branch from the repo at repo if no commit would become unreachable, i.e. every commit reachable from it is also reachable from another ref or from a worktree’s HEAD.
record_worktree_gone
Record that the worktree at path is gone. Retracts any earlier admission for the label, so a directory that later takes its place is not cleared on it.
record_worktree_owned 🔒
Record that forge created the worktree at path.
record_worktree_state 🔒
Append one state for label in this checkout. Append-only with last line winning: a rewrite would race another spawn’s record in the same process, and the file is a handful of bytes per worker lifecycle. The checkout root is stored with the path because the common dir can be shared by several checkouts.
recorded_names 🔒
Whether a recorded path that still exists names the worktree at current.
recorded_root_is_ours 🔒
Whether a record written by the checkout at recorded can speak for repo: a root that no longer exists is the one a repo move left, while a live root must be this checkout.
recorded_worktree 🔒
The record that speaks for the worktree at path, when one can be read.
remove_orphaned_worktree_dir
Delete the directory a failed remove_worktree left behind, once git no longer lists it as a worktree.
remove_owned_worktree
Remove a worktree whose removal this repo admitted - a despawn, or a refused spawn’s rollback - recording the admission before git runs. A removal that dies part-way leaves the record behind, which is what lets remove_orphaned_worktree_dir prove the leftover is ours after a repo move; a completed removal clears it.
remove_worktree
Remove the git worktree at path via git worktree remove. Run from the worktree’s PARENT (which lives inside the main worktree for claude’s <repo>/.claude/worktrees/<label> layout), since git refuses to remove the worktree it is invoked from. force adds --force, removing even a dirty worktree (the despawn path only reaches this on a verified-clean worktree, or when the caller explicitly passed force).
repair_worktree 🔒
git worktree repair <path>: git’s own remedy for a registration and link that name the path the repo has left. Only a stale registration needs it; reusing one unrepaired leaves every git command in the worktree failing.
repo_branch_names
Every branch name the repo rooted exactly at repo knows, plus how many branch refs it actually holds. None when repo is not itself a work-tree root or git could not be asked.
report_unresolved_root 🔒
The one-line report for a root git would not name, so a log grep for worktree_base_unresolved finds the path that composed the wrong way.
resolve_worktree_base 🔒
worktree_base without the memo; None when git could not answer.
same_path 🔒
Whether a path git printed and a path forge composed name the same directory. Each side canonicalizes through its parent with the name re-attached, so a symlinked spelling of a repo root (a macOS tempdir under /private, a linked checkout) cannot read as a different worktree. That matters most for a path that is gone, which is exactly where a stranded registration lives and nothing exists to canonicalize.
scrubbed_git 🔒
The command git_in_repo runs, built separately so a test can assert the scrub is on it.
spelled_from_parent 🔒
path with its parent canonicalized and its own name re-attached: the spelling a path keeps once it exists, for one that does not.
stale_agent_worktrees
Managed agent worktrees under <base>/.claude/worktrees/ untouched for at least age, for the reconciliation sweep that runs at every session spawn: it reaps what a one-shot failure or a missed completion event left behind. A lock whose reason carries a live pid (the CLI stamps one on a live isolation agent’s tree) keeps the tree out; a dead pid - a crashed CLI’s leftover, which git never prunes on its own - is reapable. Locks without a parsable pid are not ours to judge and skip. Ok(vec![]) when the worktrees dir is absent; other read errors propagate, and a repo git cannot answer about sweeps as empty - the fail-closed direction for a deleter. Feed the result to reap_agent_worktree.
survivor_tips 🔒
Every tip that would survive worktree_path’s removal: all refs plus the HEAD of each other worktree. None when git cannot be asked, which the caller treats as keep.
unpushed_commit_count 🔒
Commits HEAD reaches that no remote-tracking ref does, with a branch whose remote branch was deleted counted as pushed. None when the repo holds no remote-tracking refs to compare against, or when the query fails - both leave the verdict clean.
upstream_ref_deleted 🔒
Whether the branch’s configured upstream names a remote ref that no longer exists.
worker_root
The tree a worker spawned at cwd runs in: the repo root when the path is inside a repository, the path itself otherwise. Both are canonical, because the claude CLI derives the session JSONL’s directory from the canonical path - measured: a worktree under /tmp writes its transcript under /private/tmp - so a record holding the alias would name a directory the CLI never wrote to.
worktree_base
The checkout root the claude CLI forks a worker’s --worktree under: the repository’s main working tree, resolved from the first entry of git worktree list --porcelain - the main checkout when the project is itself a linked worktree, the repo root for a subdirectory. Measured on 2.1.280: the CLI resolves its cwd to that root before composing <root>/.claude/worktrees/<label>, so a project rooted in a linked worktree (or in a repo subdirectory) otherwise names a directory the CLI never creates (#2460). A path that is not a repository is its own base.
worktree_branch
The branch checked out at path, via git rev-parse --abbrev-ref HEAD. None on detached HEAD, a git error, or non-utf8 output. Read before a worktree is removed so its per-branch review threads clean up under the same (project, branch) key the overlay saved them with.
worktree_creation_obstacle
What git refuses to create a worker’s worktree for at path, when there is something: the worktree-<label> branch already checked out in another worktree, a path taken by something that is not this worktree, or a registration git will not reuse.
worktree_dirty_reason
Why a worktree is not safe to remove, or None when it is clean.
worktree_gitdir_resolves 🔒
Whether the worktree registered at path still points at a gitdir git can reuse.
worktree_missing_reason
Why the worktree a git worker’s spawn put it in is not usable, or None when it is there and git can read it.
worktree_record_path 🔒
worktree_registered 🔒
Whether git still lists path as one of repo’s worktrees, live or stale. A repo git cannot read answers true, the refusing direction.
worktree_registration 🔒
The registration worktree_registration finds, or None when git could not be asked. The clear treats that as listed (the refusing direction); ensure reuses the path as it stands.
worktree_removal_admitted 🔒
Whether an admitted removal is on record for this worktree and never reported complete. Where the link cannot prove ownership, this record is what the orphan clear deletes on; a live link naming this repo’s common dir clears without it.
worktree_uncommitted_reason 🔒
Uncommitted or untracked changes, or None when the tree is porcelain-clean.