Skip to content
Back to skills

Git Worktree Tidy

ASecurity

Use when fetching and pruning a worktree-based repository, removing stale local branches, worktrees, or stashes, or bringing its important branches up to date.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentspythongobashgit

Works with

  • cli

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add alleneubank/agent-profile --skill git-worktree-tidy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Git Worktree Tidy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Git Worktree Tidy
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/alleneubank-git-worktree-tidy/badge)](https://www.skillsdirectory.com/skills/alleneubank-git-worktree-tidy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: git-worktree-tidy
description: Use when fetching and pruning a worktree-based repository, removing stale local branches, worktrees, or stashes, or bringing its important branches up to date.
---

# git-worktree-tidy

Routine hygiene for bare-repo + worktree layouts. Fetches origin, prunes
gone branches and orphaned worktrees, reviews stashes, restores a missing
default-branch checkout, and fast-forwards important branches.

## When to use

User asks to "fetch prune", "clean up stale branches/worktrees", or
"update main/dev to latest" in a worktree-based repo. Stash review applies to
any repository.

## Hard Rules

- Verified-merged branches (step 3a) and clean, owner-free worktrees are
  recoverable interior work: delete them and report what was removed.
- Dirty or owned worktrees, unverifiable owners, and at-risk branches go into
  the single confirmation in step 5. Force never bypasses the owner guard.
- Every stash is at risk: nothing proves its content landed, so each one goes
  into the step 5 confirmation. Keep a stash the user wants by committing it
  to a `wip/<topic>` branch, not by leaving it stashed.
- Use `--ff-only` when updating branches. If ff-only fails, stop and ask.
- Operate from the `.bare` directory (or repo root) for branch/worktree
  management commands.
- Never infer ship status from ancestry alone; verify the forge's PR state
  and head at merge (step 3a).
- A clean checkout whose branch is verified merged is stale. Switch the
  primary (non-bare) working tree back to the default branch; remove any other
  such worktree (step 9). Never switch a dirty checkout, or one whose branch is
  not verified merged; add a new worktree at the conventional path, or skip.
- Auto-create a missing default-branch worktree only as a sibling of `.bare`
  named after that branch (`../<default>` from `.bare`). In any other layout,
  report the missing checkout and do not invent a path.

## Workflow

### 1) Locate the bare root

Determine the bare repo directory:
- If cwd contains `.bare/`, use it
- Otherwise: `git rev-parse --git-common-dir`

All branch and worktree management commands run from this directory.

### 2) Fetch + prune

```bash
git fetch --prune origin
```

Report what was pruned (deleted remote-tracking branches, updated refs).

### 3) Discover stale branches

```bash
git branch -vv | grep ': gone]'
```

Collect branch names whose upstream is gone. Also collect every other local
branch except the default: a merged PR whose remote branch was never deleted
leaves no `gone` marker, including on branches checked out in a worktree.
Step 3a classifies them all.

### 3a) Verify ship status (gone upstream ≠ merged)

A deleted upstream does not prove shipping. Squash- and rebase-merges change
SHAs, so ancestry cannot prove it either. Verify against the forge.

For each collected branch (GitHub example; substitute your forge CLI):

```bash
# Was there a merged PR from this head?
gh pr list --state all --head <branch> \
  --json number,state,mergedAt,mergeCommit \
  --jq '.[] | "#\(.number) \(.state) merged=\(.mergedAt // "no")"'

# If merged, confirm nothing was added to the branch AFTER the merge:
# the local tip should equal the PR head at merge.
gh pr view <pr> --json commits --jq '.commits[-1].oid'   # vs: git rev-parse <branch>
```

Classify each collected branch:
- **Merged (verified)** — a MERGED PR exists AND its head == local tip → shipped,
  safe to delete.
- **At risk** — no merged PR, or the tip has commits dated after the merge
  (`git show -s --format=%ci <tip>`) → genuine unshipped work. Flag it; do not
  delete without explicit approval.

If `gh`/the forge CLI is unavailable, say so and treat unverifiable branches as
**at risk** rather than assuming merged.

### 4) Discover stale worktrees

```bash
git worktree list
git worktree prune --dry-run
```

Cross-reference branches and check each candidate for dirty state:

```bash
cd <worktree-path> && git status --short
```

From the management directory, run the mandatory owner guard, including paths
whose directories are already gone:

```bash
bash <skill-dir>/scripts/worktree.sh check <worktree-path> ...
```

It emits schema-1 JSONL: path, status, session/PID owners and discovery errors.
Exit 0 means owner-free, 3 means owned, 4 means unknown (2 is usage).
It requires Python 3.9+, lsof and zmx; `SOX_BIN` selects a sox CLI outside
PATH. It checks process cwds, sox's current cwd and zmx's initial cwd,
including descendants, deleted cwds and the sox backing namespace. Set
`SOX_HOME`/`ZMX_DIR` for the host's actual namespaces; repeat for other homes.
Run on the worktree's host. OS-inaccessible processes remain unverified.
Each discovery query has a 15-second timeout. On a loaded host, set
`WORKTREE_QUERY_TIMEOUT_SECONDS` to a positive number of seconds, at most 300;
timeouts still refuse removal and prune. This limit applies to each query,
so a caller's overall deadline must also allow the remaining discovery queries.

Categorize:
- **Clean + merged + owner-free**: safe to remove (primary: step 9)
- **Dirty, branch at risk, owned or unknown**: flag with each owner/error
- **Prunable metadata**: directory gone; the same owner gate applies

### 4a) Review stashes

```bash
git stash list --format='%gd %ci %gs'
git stash show --stat --include-untracked stash@{N}
git log -1 --format='%h %s' stash@{N}^1        # the commit it was taken on
```

For each stash, record its age, base commit, and files, then read enough of
the diff to say whether that content now exists on the default branch
(`git stash show -p stash@{N} | git apply --reverse --check -` passing is
strong evidence; failing proves nothing, because later edits to the same lines
defeat it). Label each **likely landed**, **superseded** (the files or feature
were rewritten or removed since), or **unshipped**, with the evidence.

### 5) Report, then confirm only the at-risk class

Present a summary table:

```
Stale worktrees to remove:
  <path> (<branch>) [clean]
  <path> (<branch>) [AT RISK — dirty / session tool:id / PID command / unknown: error]

Stale branches:
  <branch>  [merged — PR #N, safe to delete]
  <branch>  [AT RISK — no merged PR / commits after merge; review first]

Prunable worktree metadata:
  <entry> [owner-free / AT RISK — owners or errors]

Stashes:
  stash@{N}  <age> on <base>  <files>  [likely landed | superseded | unshipped — evidence]
```

Proceed with verified-merged branches and clean, owner-free worktrees.
Batch all **at-risk** items and stashes into one confirmation with ship status
and the exact owners and teardown proposed. An owned worktree is never interior.

### 6) Remove stale worktrees

For an approved owned worktree, stop owners through their own tools first (the project's
stack-down command, `sox kill <backing-id>`, or `zmx kill <name>` in the reported
namespace). Signal only individually attributed PIDs; never a process-table
selector. Approval does not turn a discovery error into an empty inventory:
resolve it or skip. Recheck until owner-free, then remove through the guard:

```bash
bash <skill-dir>/scripts/worktree.sh remove <worktree-path>
```

The guard rechecks immediately before Git. A hit or error refuses removal;
report and skip. Add `--force` only for approved dirty state.

### 7) Delete stale branches

```bash
git branch -D <branch1> <branch2> ...
```

A merged branch still checked out in the primary working tree is deleted
after step 9 switches it off.

### 7a) Resolve stashes

Drop the stashes the user approved, highest index first so the remaining
indexes stay valid (`git stash drop stash@{N}`). For each stash the user keeps,
commit it to a `wip/<topic>` branch in a clean worktree, then drop the stash.

### 8) Prune worktree metadata

```bash
bash <skill-dir>/scripts/worktree.sh prune
```

This guards every prunable entry, including deleted directories. If any is
owned or unknown, no prune runs: report it, obtain approval for teardown, and
retry after owners are gone. Never substitute a destructive Git remove/prune.

### 9) Restore and update important branches

Discover the default branch from the remote, not by guessing `main`:

```bash
git rev-parse --abbrev-ref origin/HEAD    # origin/dev
# if missing:
git remote set-head origin -a
```

The branch name is the part after `origin/`. If `origin/HEAD` is still
unknown, report and skip restore rather than guessing.

Restore a missing checkout only for the default branch. Fast-forward every
worktree already on the default, `main`, `dev`, or a branch the user named.

Inspect `git worktree list`:

- Default (or other important) branch already checked out →
  `cd <path> && git pull --ff-only origin <branch>`.
  If that path is not the conventional `.bare` sibling, say so; do not try
  to add a second checkout (git will refuse).
- Default branch not checked out, and the primary (non-bare) working tree is
  clean on a verified-merged branch → `git switch <default>`, ff-only pull,
  then delete the merged branch.
- Default branch not checked out, git-common-dir is `.bare`, and
  `../<default>` (relative to `.bare`) does not exist:
  - local ref exists: `git worktree add ../<default> <default>`
  - no local ref: `git worktree add ../<default> -b <default> origin/<default>`
  - then ff-only pull
  Creating that checkout is interior; report the path.
- Otherwise (the conventional path exists, or the only checkouts are dirty or
  on unmerged branches) → skip and report. Do not `git switch` such a
  checkout onto the branch.

If ff-only fails, report the divergence and ask for guidance.

### 10) Final status

Show a summary: what was removed, which stashes were dropped or kept as
branches, what checkouts were restored, what was updated, any items skipped.

Files in this skill

  • SKILL.md9.6 KB
  • scripts/owners.py10.3 KB
  • scripts/worktree.sh139 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.