Skip to content

session-cleanup

Tear down the branch and worktree a conversation worked in, but only after proving the work is safe: nothing uncommitted, nothing unpushed, and the branch tip reachable from the default branch. When any of those fail it reports exactly what is unlanded and stops without touching anything. When they pass it deletes the local and remote branch, removes the worktree, and prunes. Invoke at the end of a session for "clean up", "clean up this branch", "we're done here", "tear down the worktree", or "is this safe to delete".

Pluginrepo
Invoke /repo:session-cleanup [<branch>] [--dry-run] [--keep-branch]
Allowed toolsBash Read
SourceSKILL.md

End-of-conversation teardown for the branch this session worked in. The whole skill is one question asked carefully — is every change on this branch already in the default branch? — followed by teardown only when the answer is yes.

Usage:

  • /session-cleanup — clean up the current branch and, if the session is in one, its worktree.
  • /session-cleanup task/2026-09-13-foo — clean up the named branch from elsewhere. Use this form when the session already left the worktree.
  • /session-cleanup --dry-run — run every check and print the teardown commands without executing them.
  • /session-cleanup --keep-branch — remove the worktree, leave both branches.

Deleting unlanded work is the failure that matters

Every check below exists to prevent one outcome: destroying a change that exists nowhere else. A worktree left lying around costs disk. A deleted branch carrying the only copy of an hour’s work costs the work.

So the checks are not advisory. Any single failure stops the run, and the skill reports what is unlanded and where to find it rather than asking whether to proceed anyway. Re-run it after the user lands the work.

1. Resolve the branch, the worktree, and the main checkout

Establish four facts before checking anything:

Terminal window
git rev-parse --abbrev-ref HEAD # the branch under review
git rev-parse --show-toplevel # this checkout's root
git worktree list --porcelain # every worktree and its branch
git symbolic-ref --short refs/remotes/origin/HEAD # e.g. `origin/main`

That last command yields a remote-tracking ref — origin/main, not main. Use it verbatim wherever the default branch is needed; never prefix it with origin/ again. It falls back to origin/main when origin/HEAD is unset. Comparing against a local branch instead is the stale-base mistake: git fetch advances origin/main and leaves a local main where it was. A branch argument overrides the current branch, and its worktree is the entry in git worktree list bound to it — never a path guessed from the branch name, since worktrees live wherever they were created.

Two branches are never cleaned up: the default branch itself, and a branch with no worktree and no commits of its own. Report SESSION-CLEANUP-NOTHING-TO-DO and stop.

2. Check the work is committed

Terminal window
git -C <worktree> status --porcelain

Any output at all — modified, staged, or untracked — fails the check. Report each path and stop with SESSION-CLEANUP-BLOCKED reason="uncommitted". Do not commit the changes on the user’s behalf; deciding what a stray edit means is their call.

3. Check the work is pushed and merged

Two separate facts, in this order:

Terminal window
git fetch origin --prune
git -C <worktree> log --oneline <default-ref>..<branch> # commits not in the default branch

<default-ref> is the value from step 1 exactly as git printed it.

A failed check is not a passed one. If git log exits non-zero — an unresolvable ref, no origin, a detached HEAD — its output is empty for a reason that has nothing to do with the branch being merged. Treat any non-zero exit as SESSION-CLEANUP-BLOCKED reason="unresolved" and stop. Only an empty result from a command that succeeded means anything.

An empty result from a successful check means every commit on the branch is already reachable from the default branch — merged, squash-merged, or rebased in, all of which this test catches, because it asks about content reachability rather than about the PR.

When the result is non-empty the work is unlanded. Distinguish the two cases for the report, since the user’s next move differs:

  • An open or unmerged PR. gh pr view <branch> --json number,state,url finds it. Report the PR and its state — the work is landed once that PR merges.
  • No PR at all. The commits exist only on this branch, and possibly only on this machine (git log --oneline <branch> --not --remotes shows any that were never pushed). Say so plainly; this is the case where deleting would lose work.

Either way, stop with SESSION-CLEANUP-BLOCKED reason="unmerged" and list the commits. Change nothing.

4. Tear down

Only reachable here. Run every command from the main checkout, not from the worktree being removed — a worktree cannot remove itself, and git worktree remove from inside it fails partway, leaving a registered worktree with a deleted directory.

Terminal window
git -C <main-checkout> worktree remove <worktree-path>
git -C <main-checkout> worktree prune
git -C <main-checkout> branch -d <branch>
git -C <main-checkout> push origin --delete <branch>

Four notes on the commands:

  • worktree remove without --force. It refuses on a dirty tree, which is a second backstop behind step 2. If it refuses, stop and report — do not reach for --force.
  • branch -d, never -D. The lowercase form refuses to delete an unmerged branch; after step 3 it should never refuse, and if it does, that disagreement means step 3 was wrong. Stop and report rather than overriding it.
  • The remote delete is skipped, not failed, when the branch is already gone from origin (a PR merged with auto-delete on). Mark it remote=absent.
  • --keep-branch runs the first two commands only.

Under --dry-run, print these four commands with the real paths substituted and exit with SESSION-CLEANUP-DRY-RUN ok, having mutated nothing.

5. Verify and report

Confirm the teardown actually happened rather than trusting the exit codes:

Terminal window
git -C <main-checkout> worktree list
git -C <main-checkout> branch --list <branch>
git ls-remote --heads origin <branch>

All three must come back without the branch or worktree — except under --keep-branch, which keeps the branch deliberately, so only the worktree is verified gone. Then print one line:

SESSION-CLEANUP-DONE branch=<deleted|kept> worktree=<removed|absent> remote=<deleted|absent>

The other terminal markers are SESSION-CLEANUP-BLOCKED reason="<uncommitted|unmerged>", SESSION-CLEANUP-NOTHING-TO-DO, SESSION-CLEANUP-DRY-RUN ok, SESSION-CLEANUP-PARTIAL failed=<step> when teardown half-succeeded, and ERROR reason="..." when the branch or worktree could not be resolved.

Notes

  • A tracked branch may belong to something else. Where a project’s tracker owns the branch — a work item whose closure also records an outcome, archives a claim, or updates a status file — hand off to whatever performs that closure. Tearing the branch down here does everything except that, which leaves the tracker stale. The branch name is usually what tells you: a prefix such as task/ marks a branch the tracker owns.
  • This skill looks at one branch — the one the conversation used. Finding the accumulated orphans from sessions that ended without cleaning up is a repo-wide sweep, and a different job.
  • The skill never commits, pushes a change, or merges. Its only writes are the deletions in step 4.