Git Safety Net
Prevent losing work in a tangle of branches/stashes/rebases, and recover it forensically
when something already went sideways. The commands here are all non-destructive or additive
until a step is explicitly labeled destructive — recovery must never make the loss worse.
Entry router — pick the mode from what the user is worried about
| The user says / needs… |
Go to |
| "I think I lost a commit / branch / stash", "recover the deleted X", "git reflog" |
Mode A — Recover |
| "did I lose anything?", "what worktrees/stashes/branches remain?", after a messy session |
Mode B — Audit & preserve |
| "is everything merged?", "what's still not on main?", before deleting old branches |
Mode C — Verify merged |
| "so this never happens again", starting parallel/multi-branch work |
Mode D — Prevent |
| "clean up worktrees/stashes/branches", "converge everything onto main", "only keep one main branch" |
Mode E — Retire safely |
| "an audit already said it's clean, but is anything else lost?", "check again" |
Mode B, starting at Step 0 — a repeat request usually means the first pass had the wrong scope, not that it looked carelessly |
When in doubt, run Mode B first, beginning with git_find_all_checkouts.sh (Step 0) and then
git_loss_audit.sh in each checkout it finds. Both are cheap and non-destructive, and they answer
"is anything at risk" for the whole machine rather than for whichever directory you started in.
The six load-bearing rules (internalize these; the modes apply them)
- Get the SCOPE right before you trust any verdict: every instrument here only sees the
repository it runs in.
git worktree list, git branch -a, git fsck, git stash list,
git log --not --remotes — all of them are structurally blind to an independent clone of
the same repository elsewhere on the machine. A linked worktree (git worktree add) has a
gitlink file pointing home, so it shows up; a second git clone has its own complete .git
and no back-reference, so it shows up in nothing. Run git_find_all_checkouts.sh first —
otherwise a clean audit means "clean in this one directory," which is not the question the
user asked. Real incident: a repository audited clean, every branch pushed, while 440 lines of
a working feature sat as untracked files in a sibling clone one rm -rf from gone.
Scope has a second axis: TIME. Every origin/* ref is a cached snapshot from your last
fetch, not the remote — so git fetch --all --prune before you trust any verdict that depends
on one. Read a stale cache in the right direction: for "what would be lost" it errs safe
(it can over-report unpushed work, never hide it), which is why the scripts here still run
offline. For "is this already upstream?" it fails the other way — work the remote already
has reads as unique, so you re-ship it, and if the remote improved it meanwhile your "restore"
silently reverts those improvements while looking like a rescue. Real incident: a
comparison base one day old made an already-merged change look unshipped; the rescue PR would
have reverted three fixes a later review added on top, one of them a security fix.
- Run
git_loss_audit.sh for the authoritative "what would be lost" check within a
checkout. It compares the current HEAD, every linked-worktree HEAD, local branches, and tags
against every remote, then inspects each worktree for tracked/untracked changes plus stashes and
dangling commits. The shorter git log HEAD --branches --tags --not --remotes misses a detached
HEAD in a different worktree and all uncommitted files. Ahead/behind counts do not answer
this. Run it once per checkout that rule 1 turned up, not just in the one you happen to be in.
git reflog is the first move for "I lost a commit," not fsck. Reflog records every
HEAD position (commits, checkouts, resets, rebases) for ~90 days and the lost commit is
usually in its top few lines. git fsck is the deeper net for commits reflog can't reach.
- Preserve before you clean up — and know which backup tool can actually reach the work.
Pin at-risk/dangling commits somewhere garbage collection can't reach them before deleting a
branch, running
gc, or force-pushing. Cleanup is reversible only while a ref (or the reflog
window) still points at the work. Critical asymmetry: bundle, archive, and format-patch
can only reach objects git already knows about. An untracked file that was never git added
and never stash -ued is invisible to all three — the copy on disk is the only copy, so
preserving it means literally copying the file out. Backing up "the repository" and believing
untracked work came along is how a clean-looking backup silently omits the only thing at risk.
- Verify "merged" by CONTENT, never by commit count — and know that most content checks are
also unsound. After a squash-merge,
main..branch shows the branch's original commits as
"unmerged" even though their content is on main — often 100+ phantom commits. But swapping
counts for the nearest content check is not enough: in one audit, three successive
"surely this is content-level now" instruments each returned a wrong answer — git cherry
(squash rewrites patch-ids → false UNMERGED), a three-dot diff base...ref used to ask
"what does base lack" (three-dot answers a different question and under-reported missing
files by 5×), and a file-level existence check (a file present on base can still be missing
the ref's lines). Only the trial merge (git merge-tree, what git_verify_branch_merged.sh
runs) was right every time. Diff-form and rung-by-rung reliability: references/merge_verification.md.
- For a high-stakes "is everything merged?" call, verify adversarially — ideally with a
fan-out of independent agents each trying to disprove it. One reviewer (human or model)
scanning many branches reliably misses a real gap; independent cross-checks catch it. Give at
least one agent the explicit job of widening the scope (rule 1) rather than re-checking the
branches already on the table — scope gaps hide from reviewers who accept the given frame.
Mode A — Recover lost work
A commit/branch/stash that "disappeared" is almost always still in the object store for ~90 days.
Full ladder (reflog → fsck → dangling) with exact commands and the canonical Git facts:
references/recovery_playbook.md. The 30-second version:
git reflog --date=iso | head -40 # find the lost HEAD position (most recoveries are here)
git show <sha> # CONFIRM it's the right commit before acting
git switch -c rescue/<name> <sha> # recover onto a NEW branch — never reset onto live work
If reflog doesn't show it (e.g. a dropped stash, an orphan from a rebase), fall through to
git fsck --dangling — see the playbook.
Mode B — Audit what's at risk, then preserve it
Step 0 — establish the scope (rule 1). Find every checkout of this repository on the machine,
including the independent clones no in-repo command can see:
scripts/git_find_all_checkouts.sh # defaults to this repo's parent + grandparent
DEPTH=6 scripts/git_find_all_checkouts.sh ~ # widen when clones live far from each other
It matches sibling checkouts by normalized remote URL (so the SSH and HTTPS forms of one
repository compare equal), falling back to any shared commit history whenever either the current
or a candidate checkout has no origin. That history check works for shallow clones that cannot
see the repository's true root. It never matches by directory name, because an independent clone
is usually named differently from the original (repo vs repo-hotfix), which is exactly when
name matching fails. It canonicalizes path aliases before identifying the current checkout,
disables repository-provided fsmonitor commands while inspecting candidates, and treats commits
reachable from any locally known remote-tracking ref as pushed even when a branch has no upstream.
Exit is 1 when any other checkout holds uncommitted, untracked, unpushed, or uninspectable work.
Run Steps 1–2 in each checkout it reports, then treat "nothing at risk" as a claim about all of
them, not just this one.
Maintainer verification
Run the isolated regression suite after changing checkout discovery:
uv run python -m unittest discover -s tests -p 'test_*.py'
Step 1 — audit (non-destructive). What, if anything, is at risk of loss right now:
scripts/git_loss_audit.sh # defaults to remote "origin"; pass a remote name to override
Expected output: every worktree with branch/detached state and cleanliness, plus counts of
local-only commits, dirty/unavailable worktrees, stashes, and dangling commits.
Exit is 1 when commits exist on no remote or a worktree is dirty/uninspectable; stashes and
danglers remain visible but do not alone make the audit fail. Exit 0 is therefore not permission
to delete a visible stash/dangler: triage or preserve every reported item. Do not claim cleanup is
safe until the named worktree is clean and its HEAD is proven contained or deliberately preserved.
Step 2 — preserve (additive, gc-proof). If anything showed up, make it un-loseable before
touching branches or running gc:
scripts/git_preserve_danglers.sh --patch-dir ~/git-danglers # pin + export patches
This pins every dangling commit under refs/dangling-backup/<sha> (garbage collection can never
reach a referenced commit) without cluttering git branch, and optionally writes a .patch per
non-stash commit. For a specific important commit, also give it the full treatment — local
branch and a pushed remote branch and a git format-patch file — so a single disk or a
single git gc can't take it. Details + why triple-backup: references/recovery_playbook.md.
Untracked files need a different tool — plain copying (rule 4). Everything above moves git
objects; a file git was never told about is not one. Preserve those explicitly, and keep the
three channels separate so a later reader knows what each restores:
git -C <checkout> status --porcelain | grep '^??' # what is untracked
cp <each-untracked-path> <backup>/ # the ONLY copy — plain cp
git -C <checkout> diff > <backup>/uncommitted.diff # tracked-but-uncommitted
git -C <checkout> bundle create <backup>/history.bundle origin/main..HEAD # unpushed commits
git bundle verify <backup>/history.bundle # prove it restores
Write a one-paragraph README beside them saying where they came from, which branch, and when the
session stopped. A backup nobody can interpret six weeks later is only slightly better than none —
and the person reading it will not be the person who made it.
Mode C — Verify everything is merged (without being fooled by counts)
The trap: a stale branch shows "173 commits ahead of main" yet every line is already on main
(squash-merge artifact). Never conclude "unmerged" from counts. Per-branch content check:
scripts/git_verify_branch_merged.sh <branch> [<base>] # base defaults to origin/main
This mode is the one direction where a stale base is unsafe (rule 1): judged against yesterday's
origin/main, a branch whose content landed hours ago still reads UNMERGED, and "rescuing" it
re-applies an older version over whatever was built on top. The script fetches first for exactly
that reason — but if the fetch fails it falls back to cached refs and says so on stderr only.
Treat that line as a blocker, not a footnote: rerun once the network is back before acting on the
verdict. Comparing by hand (git diff origin/main <branch>, git log origin/main..<branch>) has
no such safety net at all — fetch yourself first, every time.
It reports MERGED (ancestor) or MERGED (content contained) — safe to delete — versus
UNMERGED / NEEDS REVIEW, listing the files the branch would still change. The verdict is sound,
not heuristic: it does a trial 3-way merge of the branch into the base with git merge-tree
(in memory, no checkout) and only says "safe to delete" when that merge changes nothing — so a
squash-merged branch reads MERGED despite a nonzero commit count, while a revert/edit/new-file the
base lacks reads UNMERGED. It is safety-biased: anything it can't prove contained is reported
for review, because a false "merged" loses work while a false "unmerged" only costs a look. Full
technique (and why --find-object/blob heuristics are unsound for auto-decisions), plus the
adversarial multi-agent verification pattern for a whole repo of branches (read-only agents,
one per batch, each told to falsify "everything is merged," every finding independently
re-checked): references/merge_verification.md.
Mode D — Prevent the disaster
The habits that keep a branch tangle from ever stranding work:
references/prevention_practices.md. The load-bearing few:
- Commit before you switch — neither
git stash nor git worktree. Uncommitted work is what
gets stranded: a git stash you later can't find, or edits a switch buries. Commit each line
of work to its own branch and push it early (a committed, pushed branch can't be orphaned), then
bring it where you need it live by merging — not by stashing, and not by spinning up a second
git worktree checkout (which is one more place to forget work and won't even have your
gitignored deps). A shared working tree with commit-then-switch discipline is the safe default.
- If you truly need a second checkout, make it a worktree — never a second
git clone. Both
are extra places to forget work, which is why commit-then-switch above is still the default. But
the failure modes are not equal: a linked worktree announces itself in git worktree list, so
every audit finds it, while an independent clone is invisible to every command run from the
original repository. Choosing clone for a few days of parallel work quietly opts out of all
the safety tooling. When a clone already exists (a colleague made it, a script made it, you
inherited it), register it somewhere the team actually reads and retire it the day it's done —
and until then, treat it as an audit target in its own right, not as a scratch directory.
- Push a work-in-progress branch to a remote early. The one commit only on a local branch is
the only commit that a dead laptop actually loses.
- Confirm the current branch before committing (
git branch --show-current) — a fix committed
onto the wrong feature branch is invisible to its real PR and easy to lose on cleanup.
- In a shared tree, never aim a destructive command at "the current branch" — name the branch
explicitly.
reset --hard, merge, and rebase all act on whatever is checked out at the
instant they run, so a branch check is stale the moment it returns: a parallel session can
switch in between, and your command lands on their branch. This is the inverse of the
bullet below (that one protects your work from their switch; this one protects theirs
from your command), and re-checking harder does not fix it — the race is inherent. Use the
checkout-independent forms instead, which name their target and never touch the working tree:git branch -f <branch> <target> # instead of: switch <branch> && reset --hard <target>
git fetch origin <branch>:<branch> # fast-forward a branch you are not on
git push origin <sha>:refs/heads/<branch>
Real incident: a reset --hard origin/main issued seconds after git branch --show-current
said main landed on a parallel session's feature branch and moved it back two commits; the
follow-up "repair" then missed again because the tree had been switched a second time.
git branch -f fixed both in one shot precisely because it never consults the checkout.
- If a parallel session switched the shared tree onto its branch and stranded your uncommitted
work there, don't commit onto their branch — carry your edits to a branch off the base
(
git checkout origin/main -b …, after git diff --quiet proves your files match across bases),
commit only your explicit paths, then switch the tree back to their branch to restore their state.
- If a parallel session is actively writing the shared tree — files keep appearing while you
work — don't
switch, add, or reset at all: each would either strand their uncommitted work
or trip a worktree guard. When your own change is self-contained (new files, or edits that belong
on origin/main rather than on their in-progress tree), build the commit with plumbing that never
touches the working tree, then push it to a branch and open a PR:export GIT_INDEX_FILE=$(mktemp) # a scratch index — the tree's real index is untouched
git read-tree origin/main # start from the pushed base, not the dirty tree
git update-index --add --cacheinfo 100644,"$(git hash-object -w path/to/file)",path/to/file
tree=$(git write-tree)
commit=$(git commit-tree "$tree" -p origin/main -m "…") # HEAD does not move
unset GIT_INDEX_FILE
git push origin "$commit":refs/heads/<branch> # open the PR from here
The sequence reads and writes only the object store and a throwaway index, so git status in the
shared tree is byte-for-byte unchanged and the other session never sees a ripple. This is the
escape hatch for when commit-then-switch is off the table because someone else holds the tree.
- Before any rebase or branch-delete, run the Mode B audit. Ten seconds; it's the difference
between "nothing to lose" and finding out after gc.
- Before bumping a shared version/lockfile, check the base's current value so two parallel
branches don't both claim the same bump (a silent collision that blocks the later change from
shipping).
Mode E — Retire worktrees, stashes, and branches safely
The opposite worry from Mode A: not "I lost something" but "these leftovers are piling up —
which can I destroy?" Deleting is trivial; proving each item is superseded is the work.
Start with git_find_all_checkouts.sh — git worktree list --porcelain alone will not show an
independent clone, and those are the leftovers most likely to be forgotten — then git_loss_audit.sh
inside each checkout it reports. Treat every checkout as an independent place where uncommitted or
detached work can hide. Then triage, backup, and retire:
Step 1 — classify each leftover: live WIP, or superseded draft? Evidence ladder, strongest first:
git cherry <base> <branch> — judges by patch content, not message text. Every commit
showing - is already on the base (survives rebases and reworded messages); any + needs
the next rungs. Never grep commit messages to decide this — the same work often lands under
a different message.
- Same-file supersession check — for a stash or
+ commit touching files that were later
reworked on the base: extract its version of the file and compare with the base's current
version (git show <ref>:<path> | wc -l vs git show <base>:<path> | wc -l, then spot-diff).
If the base's version is a superset (has everything the leftover has, plus later work),
the leftover is a superseded draft. Real case: a stash labeled "unfinished dev" held a 1128-line
renderer; main's version was 1151 lines — the same functions plus a later feature parameter.
Restoring that stash would have been a regression, not a recovery.
- Function/marker-level probe — grep the base for the leftover's distinctive additions
(
def new_helper, a constant, an error string). All present on the base → superseded.
This catches "absorbed into a refactor" cases where file shapes changed too much for rung 2.
Anything you cannot prove superseded stays alive (same safety bias as Mode C: a false "superseded"
loses work; a false "still live" costs a branch name). One warning that changes verdicts: the
leftover's label is not evidence — a stash named "unfinished development" can be a fully-landed
early draft; judge content against the current base, never the name. Worked examples of all three
rungs (including the squash-artifact and absorbed-into-refactor cases):
references/merge_verification.md § Supersession triage.
Step 2 — pin true orphans, then back up every addressable ref:
scripts/git_preserve_danglers.sh --patch-dir <backup-dir>/dangling-patches
scripts/git_export_before_drop.sh --all-stashes --all-refs --out <backup-dir>
The first command makes unreferenced commits reachable; --all-refs then captures branch, stash,
hidden-backup, and linked-worktree HEAD refs in one verified bundle. For a small targeted cleanup,
use repeated --branch instead. The exporter never drops or deletes anything.
Step 3 — destroy, in the safe order:
Stashes: drop from the highest index down (drop stash@{2} before stash@{1}) — indices
shift as you drop, and top-down keeps every number meaning what your backup filenames say.
Linked worktrees: require a clean git -C <path> status --short --branch, record its exact HEAD,
prove that HEAD is contained/superseded, then use git worktree remove <absolute-path> without
--force and re-run git worktree list. Never remove the primary/current checkout. Follow
references/merge_verification.md § Worktree retirement.
Local branches: prefer git branch -d (refuses unmerged); use -D only for items Step 1
proved superseded, backed up, and the user authorized deleting. A squash-merge is the usual
reason -d refuses a branch whose content is fully merged: -d judges by commit ancestry, and
the squash replaced the branch's commits with one new-SHA commit, so ancestry is broken even
though every line landed. That is not license to reach for -D reflexively — it means fall back
to Step 1's content check (git cherry, superset diff) and only -D once that proves
containment. Delete remote branches only after re-verifying the exact remote and repository
visibility/ownership.
Independent clones: there is no safe git-level command — only rm -rf, which git cannot
undo. git worktree remove does not apply (it isn't a worktree) and refuses to help, so the
usual "the tool will stop me if it's unsafe" backstop is absent here. Make the check explicit
instead: gate the deletion on the backup actually existing, so a missing file aborts rather
than being noticed afterwards.
for f in <backup>/<untracked-file> <backup>/uncommitted.diff <backup>/history.bundle; do
[ -s "$f" ] || { echo "MISSING: $f — refusing to delete"; exit 1; }
done
rm -rf <clone-path>
Prefer deleting one clone at a time with its own verification over a loop across several — a
glob that deletes five directories has five chances to be wrong and reports none of them.
Step 4 — after the delete, re-check by content, not by filename. When a cleanup (or a batch of
squash-merges) is already done and the question becomes "did any of it drop work?", the naming-based
check that felt sufficient — comm over git ls-tree filenames, "every file is still on main" — is
not enough: identical filenames say nothing about identical content. A file the deleted branch and
the survivor both have can still differ line-for-line. Re-verify at blob level, and read the diff in
the right direction:
git diff <survivor-ref> <deleted-or-merged-tip> # survivor first, the gone thing second
Lines marked - are on the survivor but not the tip → the survivor is a superset (safe: it has
everything the tip had, and more). Lines marked + are on the tip but not the survivor → candidate
loss — run each through Step 1's ladder: is that symbol on the survivor under a different shape (a
rename or refactor, not a deletion)? A diff that is mostly - with a few + is the fingerprint of
"the survivor moved on and the deleted branch was an older version" — a merge that succeeded, not
work lost. Apply the same test to any preserved backup: byte-identical or survivor-superset is safe;
a line the survivor genuinely lacks anywhere is the one to escalate.
Recovery, if you regret it: patches re-apply with git apply; the untracked tar extracts
in place; the bundle restores full history via git fetch <file>.bundle <branch>:restored/<branch>.
Scripts (execute these; they are non-destructive unless noted)
| Script |
Does |
Mutates? |
scripts/git_find_all_checkouts.sh [root ...] |
Find every checkout of this repo on the machine — including independent clones invisible to git worktree list — and flag those holding uncommitted/untracked/unpushed work, plus how stale each one's cached remote refs are (STALE_AFTER=<s>, default 3600) |
Nothing (read-only, no fetch) |
scripts/git_loss_audit.sh [remote] |
Refresh one remote, then report every worktree, local-only commit, stash, and dangler |
Remote-tracking refs only |
scripts/git_preserve_danglers.sh [--patch-dir DIR] |
Pin danglers to refs/dangling-backup/, optional patches |
Adds refs only (never deletes/gc) |
scripts/git_verify_branch_merged.sh <branch> [base] |
Refresh remotes, then give a content-level MERGED/UNMERGED verdict |
Remote-tracking refs only |
scripts/git_export_before_drop.sh [--all-stashes] [--stash N] [--branch B] [--all-refs] [--out DIR] |
Export stashes plus selected branches or every current ref into verified bundles |
Writes backup files only (never drops/deletes) |
All five run from the repository root. They only ever find, fetch, log, diff, show,
status, cat-file, rev-list, rev-parse, fsck, for-each-ref, remote get-url,
stash show, archive, bundle create/verify, and (preserve only) update-ref — never
checkout, reset, push, stash drop, branch -d, or gc, so they are safe to run in a
dirty tree or alongside other agents. git_find_all_checkouts.sh additionally never fetches, so
it works offline and behind a proxy.
Troubleshooting
- An audit came back clean but the user still thinks something is missing — believe them and
suspect scope, not thoroughness. The in-repo instruments were probably all correct about the
one directory they could see. Run Step 0 (
git_find_all_checkouts.sh) before re-running anything
you already ran; repeating a correctly-executed check in the wrong scope returns the same clean
answer with more confidence behind it, which is worse than the first pass.
git_find_all_checkouts.sh finds nothing, but you're fairly sure another copy exists — three
likely causes, in order: (1) the copy lives outside the default roots (pass an explicit root such
as ~, and raise DEPTH); (2) it sits under a pruned path — the sweep skips node_modules,
.venv, vendor, .terraform; (3) its origin points somewhere else entirely (a fork, or a
path remote), so remote matching rejects it — check with git -C <suspect> remote -v and compare
root commits by hand: git rev-list --max-parents=0 HEAD. A copy made by cp -r before the repo
had any remote will only match on root commit.
git_loss_audit.sh reports dangling commits that look like old stashes — expected after
stash-heavy work. They're reflog-reachable now; pin them with git_preserve_danglers.sh if you
want them past the gc window, then inspect with git show <sha> at leisure.
- A branch shows huge "commits ahead" but you suspect it's merged — trust
git_verify_branch_merged.sh (content), not the count. See Mode C.
git fetch in a script hangs behind a proxy / offline — loss detection still works on
cached remote refs, because a stale cache can only over-report unpushed work. Merge and
supersession verdicts (Mode C, Mode E) are the exception and genuinely need a fetch; without
one, say so in the report rather than presenting the verdict as settled.
- Your work looks unmerged, but the repository moved while you were working — check the clock
before you rescue anything:
git_find_all_checkouts.sh prints when each checkout last fetched,
and git log --oneline <cached-base>..origin/main after a fresh fetch shows what arrived
meanwhile. A long session is the risk window — the base you compared against at the start can be
many hours old by the end. Symptom to recognise: a change you know you committed appears absent
upstream, so you prepare to re-ship it. Fetch first, then compare by content; if it did land,
check whether anyone improved it before re-applying your version over theirs.
- You're on a detached HEAD after checking out a commit — that commit is safe as long as you
git switch -c <branch> HEAD (or the reflog remembers it for ~90 days). Don't leave important
new work on a detached HEAD across a gc.
- Only one worktree remains after cleanup —
git worktree list always includes the primary
repository checkout. Do not delete it merely to make the count zero; the goal is one maintained
checkout, not no checkout.
refs/dangling-backup/* refs are cluttering things later — once you've confirmed (Mode C)
their content is on a remote, delete them with git for-each-ref --format='%(refname)' refs/dangling-backup/ | xargs -n1 git update-ref -d. Only after you've verified.
Next step
After recovery/audit, if the repo also needs routine setup, safe commit/push, conflict handling,
or handoff hygiene, that's the auto-repo-setup skill's job (invoke /auto-repo-setup) — this
skill is the forensic/recovery layer, that one is the routine-workflow layer.
1---2name: git-safety-net3description: Audits, preserves, recovers, and safely retires local Git state: unpushed or wrong-branch commits, dirty or detached worktrees, forgotten duplicate clones of the same repo, untracked work no bundle can back up, orphaned stashes, dangling commits, stale branches, and squash/rebase merge uncertainty. Use when the user fears work was lost; asks to recover a commit or branch; asks whether a worktree, clone, or scratch directory can be deleted; wants everything converged onto one main branch; or needs proof that cleanup will not drop work. Use it even after an audit reported clean — the usual gap is scope: every in-repo command is blind to a second clone elsewhere on disk. Triggers on "did I lose work", "is everything merged", "is anything else lost", "safe to delete this clone", "clean up old branches/stashes", "only keep one main branch", "git reflog", "dangling commits", "分支灾难", "误删分支/commit", "worktree 能删吗", "还有没有丢的东西", "只保留一个主分支". Covers local-Git forensics, not GitHub PR/API operations or routine sync.4---56# Git Safety Net78Prevent losing work in a tangle of branches/stashes/rebases, and recover it forensically9when something already went sideways. The commands here are all **non-destructive or additive**10until a step is explicitly labeled destructive — recovery must never make the loss worse.1112## Entry router — pick the mode from what the user is worried about1314| The user says / needs… | Go to |15|---|---|16| "I think I lost a commit / branch / stash", "recover the deleted X", "git reflog" | **Mode A — Recover** |17| "did I lose anything?", "what worktrees/stashes/branches remain?", after a messy session | **Mode B — Audit & preserve** |18| "is everything merged?", "what's still not on main?", before deleting old branches | **Mode C — Verify merged** |19| "so this never happens again", starting parallel/multi-branch work | **Mode D — Prevent** |20| "clean up worktrees/stashes/branches", "converge everything onto main", "only keep one main branch" | **Mode E — Retire safely** |21| "an audit already said it's clean, but is anything *else* lost?", "check again" | **Mode B, starting at Step 0** — a repeat request usually means the first pass had the wrong scope, not that it looked carelessly |2223When in doubt, **run Mode B first**, beginning with `git_find_all_checkouts.sh` (Step 0) and then24`git_loss_audit.sh` in each checkout it finds. Both are cheap and non-destructive, and they answer25"is anything at risk" for the whole machine rather than for whichever directory you started in.2627## The six load-bearing rules (internalize these; the modes apply them)28291. **Get the SCOPE right before you trust any verdict: every instrument here only sees the30 repository it runs in.** `git worktree list`, `git branch -a`, `git fsck`, `git stash list`,31 `git log --not --remotes` — all of them are structurally blind to an **independent clone** of32 the same repository elsewhere on the machine. A linked worktree (`git worktree add`) has a33 gitlink *file* pointing home, so it shows up; a second `git clone` has its own complete `.git`34 and no back-reference, so it shows up in **nothing**. Run `git_find_all_checkouts.sh` first —35 otherwise a clean audit means "clean in this one directory," which is not the question the36 user asked. Real incident: a repository audited clean, every branch pushed, while 440 lines of37 a working feature sat as untracked files in a sibling clone one `rm -rf` from gone.38 **Scope has a second axis: TIME.** Every `origin/*` ref is a cached snapshot from your last39 fetch, not the remote — so `git fetch --all --prune` before you trust any verdict that depends40 on one. Read a stale cache in the right direction: for *"what would be lost"* it errs safe41 (it can over-report unpushed work, never hide it), which is why the scripts here still run42 offline. For *"is this already upstream?"* it fails the other way — work the remote already43 has reads as unique, so you re-ship it, and if the remote improved it meanwhile your "restore"44 silently **reverts** those improvements while looking like a rescue. Real incident: a45 comparison base one day old made an already-merged change look unshipped; the rescue PR would46 have reverted three fixes a later review added on top, one of them a security fix.472. **Run `git_loss_audit.sh` for the authoritative "what would be lost" check *within a48 checkout*.** It compares the current HEAD, every linked-worktree HEAD, local branches, and tags49 against every remote, then inspects each worktree for tracked/untracked changes plus stashes and50 dangling commits. The shorter `git log HEAD --branches --tags --not --remotes` misses a detached51 HEAD in a different worktree and all uncommitted files. Ahead/behind counts do **not** answer52 this. Run it once per checkout that rule 1 turned up, not just in the one you happen to be in.533. **`git reflog` is the first move for "I lost a commit," not `fsck`.** Reflog records every54 HEAD position (commits, checkouts, resets, rebases) for ~90 days and the lost commit is55 usually in its top few lines. `git fsck` is the deeper net for commits reflog can't reach.564. **Preserve before you clean up — and know which backup tool can actually reach the work.**57 Pin at-risk/dangling commits somewhere garbage collection can't reach them *before* deleting a58 branch, running `gc`, or force-pushing. Cleanup is reversible only while a ref (or the reflog59 window) still points at the work. **Critical asymmetry: `bundle`, `archive`, and `format-patch`60 can only reach objects git already knows about.** An untracked file that was never `git add`ed61 and never `stash -u`ed is invisible to all three — the copy on disk is the only copy, so62 preserving it means literally copying the file out. Backing up "the repository" and believing63 untracked work came along is how a clean-looking backup silently omits the only thing at risk.645. **Verify "merged" by CONTENT, never by commit count — and know that most content checks are65 also unsound.** After a squash-merge, `main..branch` shows the branch's original commits as66 "unmerged" even though their content is on main — often 100+ phantom commits. But swapping67 counts for the *nearest* content check is not enough: in one audit, three successive68 "surely this is content-level now" instruments each returned a wrong answer — `git cherry`69 (squash rewrites patch-ids → false UNMERGED), a **three-dot** `diff base...ref` used to ask70 "what does base lack" (three-dot answers a different question and **under-reported missing71 files by 5×**), and a file-level existence check (a file present on base can still be missing72 the ref's lines). Only the trial merge (`git merge-tree`, what `git_verify_branch_merged.sh`73 runs) was right every time. Diff-form and rung-by-rung reliability: **[references/merge_verification.md](references/merge_verification.md)**.746. **For a high-stakes "is everything merged?" call, verify adversarially — ideally with a75 fan-out of independent agents each trying to *disprove* it.** One reviewer (human or model)76 scanning many branches reliably misses a real gap; independent cross-checks catch it. Give at77 least one agent the explicit job of widening the *scope* (rule 1) rather than re-checking the78 branches already on the table — scope gaps hide from reviewers who accept the given frame.7980## Mode A — Recover lost work8182A commit/branch/stash that "disappeared" is almost always still in the object store for ~90 days.83Full ladder (reflog → fsck → dangling) with exact commands and the canonical Git facts:84**[references/recovery_playbook.md](references/recovery_playbook.md)**. The 30-second version:8586```bash87git reflog --date=iso | head -40 # find the lost HEAD position (most recoveries are here)88git show <sha> # CONFIRM it's the right commit before acting89git switch -c rescue/<name> <sha> # recover onto a NEW branch — never reset onto live work90```9192If reflog doesn't show it (e.g. a dropped stash, an orphan from a rebase), fall through to93`git fsck --dangling` — see the playbook.9495## Mode B — Audit what's at risk, then preserve it9697**Step 0 — establish the scope (rule 1).** Find every checkout of this repository on the machine,98including the independent clones no in-repo command can see:99100```bash101scripts/git_find_all_checkouts.sh # defaults to this repo's parent + grandparent102DEPTH=6 scripts/git_find_all_checkouts.sh ~ # widen when clones live far from each other103```104105It matches sibling checkouts by normalized remote URL (so the SSH and HTTPS forms of one106repository compare equal), falling back to **any shared commit history** whenever either the current107or a candidate checkout has no `origin`. That history check works for shallow clones that cannot108see the repository's true root. It never matches by directory name, because an independent clone109is usually named differently from the original (`repo` vs `repo-hotfix`), which is exactly when110name matching fails. It canonicalizes path aliases before identifying the current checkout,111disables repository-provided fsmonitor commands while inspecting candidates, and treats commits112reachable from any locally known remote-tracking ref as pushed even when a branch has no upstream.113Exit is 1 when any *other* checkout holds uncommitted, untracked, unpushed, or uninspectable work.114Run Steps 1–2 in **each** checkout it reports, then treat "nothing at risk" as a claim about all of115them, not just this one.116117### Maintainer verification118119Run the isolated regression suite after changing checkout discovery:120121```bash122uv run python -m unittest discover -s tests -p 'test_*.py'123```124125**Step 1 — audit (non-destructive).** What, if anything, is at risk of loss right now:126127```bash128scripts/git_loss_audit.sh # defaults to remote "origin"; pass a remote name to override129```130131Expected output: every worktree with branch/detached state and cleanliness, plus counts of132**local-only commits**, **dirty/unavailable worktrees**, **stashes**, and **dangling commits**.133Exit is 1 when commits exist on no remote or a worktree is dirty/uninspectable; stashes and134danglers remain visible but do not alone make the audit fail. Exit 0 is therefore not permission135to delete a visible stash/dangler: triage or preserve every reported item. Do not claim cleanup is136safe until the named worktree is clean and its HEAD is proven contained or deliberately preserved.137138**Step 2 — preserve (additive, gc-proof).** If anything showed up, make it un-loseable *before*139touching branches or running gc:140141```bash142scripts/git_preserve_danglers.sh --patch-dir ~/git-danglers # pin + export patches143```144145This pins every dangling commit under `refs/dangling-backup/<sha>` (garbage collection can never146reach a referenced commit) without cluttering `git branch`, and optionally writes a `.patch` per147non-stash commit. For a *specific* important commit, also give it the full treatment — local148branch **and** a pushed remote branch **and** a `git format-patch` file — so a single disk or a149single `git gc` can't take it. Details + why triple-backup: **[references/recovery_playbook.md](references/recovery_playbook.md)**.150151**Untracked files need a different tool — plain copying (rule 4).** Everything above moves *git152objects*; a file git was never told about is not one. Preserve those explicitly, and keep the153three channels separate so a later reader knows what each restores:154155```bash156git -C <checkout> status --porcelain | grep '^??' # what is untracked157cp <each-untracked-path> <backup>/ # the ONLY copy — plain cp158git -C <checkout> diff > <backup>/uncommitted.diff # tracked-but-uncommitted159git -C <checkout> bundle create <backup>/history.bundle origin/main..HEAD # unpushed commits160git bundle verify <backup>/history.bundle # prove it restores161```162163Write a one-paragraph `README` beside them saying where they came from, which branch, and when the164session stopped. A backup nobody can interpret six weeks later is only slightly better than none —165and the person reading it will not be the person who made it.166167## Mode C — Verify everything is merged (without being fooled by counts)168169The trap: a stale branch shows "173 commits ahead of main" yet every line is already on main170(squash-merge artifact). Never conclude "unmerged" from counts. Per-branch content check:171172```bash173scripts/git_verify_branch_merged.sh <branch> [<base>] # base defaults to origin/main174```175176This mode is the one direction where a stale base is *unsafe* (rule 1): judged against yesterday's177`origin/main`, a branch whose content landed hours ago still reads UNMERGED, and "rescuing" it178re-applies an older version over whatever was built on top. The script fetches first for exactly179that reason — but if the fetch fails it falls back to cached refs and says so **on stderr only**.180Treat that line as a blocker, not a footnote: rerun once the network is back before acting on the181verdict. Comparing by hand (`git diff origin/main <branch>`, `git log origin/main..<branch>`) has182no such safety net at all — fetch yourself first, every time.183184It reports **MERGED (ancestor)** or **MERGED (content contained)** — safe to delete — versus185**UNMERGED / NEEDS REVIEW**, listing the files the branch would still change. The verdict is sound,186not heuristic: it does a trial 3-way merge of the branch *into* the base with `git merge-tree`187(in memory, no checkout) and only says "safe to delete" when that merge changes nothing — so a188squash-merged branch reads MERGED despite a nonzero commit count, while a revert/edit/new-file the189base lacks reads UNMERGED. It is **safety-biased**: anything it can't prove contained is reported190for review, because a false "merged" loses work while a false "unmerged" only costs a look. Full191technique (and why `--find-object`/blob heuristics are unsound for auto-decisions), plus the192**adversarial multi-agent verification** pattern for a whole repo of branches (read-only agents,193one per batch, each told to *falsify* "everything is merged," every finding independently194re-checked): **[references/merge_verification.md](references/merge_verification.md)**.195196## Mode D — Prevent the disaster197198The habits that keep a branch tangle from ever stranding work:199**[references/prevention_practices.md](references/prevention_practices.md)**. The load-bearing few:200201- **Commit before you switch — neither `git stash` nor `git worktree`.** Uncommitted work is what202 gets stranded: a `git stash` you later can't find, or edits a `switch` buries. Commit each line203 of work to its own branch and push it early (a committed, pushed branch can't be orphaned), then204 bring it where you need it *live* by merging — not by stashing, and not by spinning up a second205 `git worktree` checkout (which is one more place to forget work and won't even have your206 gitignored deps). A shared working tree with commit-then-switch discipline is the safe default.207- **If you truly need a second checkout, make it a worktree — never a second `git clone`.** Both208 are extra places to forget work, which is why commit-then-switch above is still the default. But209 the failure modes are not equal: a linked worktree announces itself in `git worktree list`, so210 every audit finds it, while an independent clone is invisible to every command run from the211 original repository. Choosing `clone` for a few days of parallel work quietly opts out of all212 the safety tooling. When a clone already exists (a colleague made it, a script made it, you213 inherited it), register it somewhere the team actually reads and retire it the day it's done —214 and until then, treat it as an audit target in its own right, not as a scratch directory.215- **Push a work-in-progress branch to a remote early.** The one commit only on a local branch is216 the only commit that a dead laptop actually loses.217- **Confirm the current branch before committing** (`git branch --show-current`) — a fix committed218 onto the wrong feature branch is invisible to its real PR and easy to lose on cleanup.219- **In a shared tree, never aim a destructive command at "the current branch" — name the branch220 explicitly.** `reset --hard`, `merge`, and `rebase` all act on *whatever is checked out at the221 instant they run*, so a branch check is stale the moment it returns: a parallel session can222 `switch` in between, and your command lands on **their** branch. This is the inverse of the223 bullet below (that one protects *your* work from *their* switch; this one protects *theirs*224 from *your* command), and re-checking harder does not fix it — the race is inherent. Use the225 checkout-independent forms instead, which name their target and never touch the working tree:226 ```bash227 git branch -f <branch> <target> # instead of: switch <branch> && reset --hard <target>228 git fetch origin <branch>:<branch> # fast-forward a branch you are not on229 git push origin <sha>:refs/heads/<branch>230 ```231 Real incident: a `reset --hard origin/main` issued seconds after `git branch --show-current`232 said `main` landed on a parallel session's feature branch and moved it back two commits; the233 follow-up "repair" then missed *again* because the tree had been switched a second time.234 `git branch -f` fixed both in one shot precisely because it never consults the checkout.235- **If a parallel session switched the shared tree onto its branch** and stranded your uncommitted236 work there, don't commit onto their branch — carry your edits to a branch off the base237 (`git checkout origin/main -b …`, after `git diff --quiet` proves your files match across bases),238 commit only your explicit paths, then switch the tree back to their branch to restore their state.239- **If a parallel session is *actively* writing the shared tree** — files keep appearing while you240 work — don't `switch`, `add`, or `reset` at all: each would either strand their uncommitted work241 or trip a worktree guard. When your own change is self-contained (new files, or edits that belong242 on `origin/main` rather than on their in-progress tree), build the commit with plumbing that never243 touches the working tree, then push it to a branch and open a PR:244 ```bash245 export GIT_INDEX_FILE=$(mktemp) # a scratch index — the tree's real index is untouched246 git read-tree origin/main # start from the pushed base, not the dirty tree247 git update-index --add --cacheinfo 100644,"$(git hash-object -w path/to/file)",path/to/file248 tree=$(git write-tree)249 commit=$(git commit-tree "$tree" -p origin/main -m "…") # HEAD does not move250 unset GIT_INDEX_FILE251 git push origin "$commit":refs/heads/<branch> # open the PR from here252 ```253 The sequence reads and writes only the object store and a throwaway index, so `git status` in the254 shared tree is byte-for-byte unchanged and the other session never sees a ripple. This is the255 escape hatch for when commit-then-switch is off the table because someone else holds the tree.256- **Before any rebase or branch-delete, run the Mode B audit.** Ten seconds; it's the difference257 between "nothing to lose" and finding out after gc.258- **Before bumping a shared version/lockfile, check the base's current value** so two parallel259 branches don't both claim the same bump (a silent collision that blocks the later change from260 shipping).261262## Mode E — Retire worktrees, stashes, and branches safely263264The opposite worry from Mode A: not "I lost something" but "these leftovers are piling up —265which can I destroy?" Deleting is trivial; **proving each item is superseded is the work**.266Start with `git_find_all_checkouts.sh` — `git worktree list --porcelain` alone will not show an267independent clone, and those are the leftovers most likely to be forgotten — then `git_loss_audit.sh`268inside each checkout it reports. Treat every checkout as an independent place where uncommitted or269detached work can hide. Then triage, backup, and retire:270271**Step 1 — classify each leftover: live WIP, or superseded draft?** Evidence ladder, strongest first:2722731. **`git cherry <base> <branch>`** — judges by *patch content*, not message text. Every commit274 showing `-` is already on the base (survives rebases and reworded messages); any `+` needs275 the next rungs. Never grep commit messages to decide this — the same work often lands under276 a different message.2772. **Same-file supersession check** — for a stash or `+` commit touching files that were later278 reworked on the base: extract its version of the file and compare with the base's current279 version (`git show <ref>:<path> | wc -l` vs `git show <base>:<path> | wc -l`, then spot-diff).280 If the base's version is a **superset** (has everything the leftover has, plus later work),281 the leftover is a superseded draft. Real case: a stash labeled "unfinished dev" held a 1128-line282 renderer; main's version was 1151 lines — the same functions *plus* a later feature parameter.283 Restoring that stash would have been a regression, not a recovery.2843. **Function/marker-level probe** — grep the base for the leftover's distinctive additions285 (`def new_helper`, a constant, an error string). All present on the base → superseded.286 This catches "absorbed into a refactor" cases where file shapes changed too much for rung 2.287288Anything you cannot prove superseded stays alive (same safety bias as Mode C: a false "superseded"289loses work; a false "still live" costs a branch name). One warning that changes verdicts: **the290leftover's label is not evidence** — a stash named "unfinished development" can be a fully-landed291early draft; judge content against the current base, never the name. Worked examples of all three292rungs (including the squash-artifact and absorbed-into-refactor cases):293**[references/merge_verification.md](references/merge_verification.md)** § Supersession triage.294295**Step 2 — pin true orphans, then back up every addressable ref:**296297```bash298scripts/git_preserve_danglers.sh --patch-dir <backup-dir>/dangling-patches299scripts/git_export_before_drop.sh --all-stashes --all-refs --out <backup-dir>300```301302The first command makes unreferenced commits reachable; `--all-refs` then captures branch, stash,303hidden-backup, and linked-worktree HEAD refs in one verified bundle. For a small targeted cleanup,304use repeated `--branch` instead. The exporter never drops or deletes anything.305306**Step 3 — destroy, in the safe order:**307308- Stashes: drop from the **highest index down** (`drop stash@{2}` before `stash@{1}`) — indices309 shift as you drop, and top-down keeps every number meaning what your backup filenames say.310- Linked worktrees: require a clean `git -C <path> status --short --branch`, record its exact HEAD,311 prove that HEAD is contained/superseded, then use `git worktree remove <absolute-path>` **without312 `--force`** and re-run `git worktree list`. Never remove the primary/current checkout. Follow313 **[references/merge_verification.md](references/merge_verification.md)** § Worktree retirement.314- Local branches: prefer `git branch -d` (refuses unmerged); use `-D` only for items Step 1315 proved superseded, backed up, and the user authorized deleting. **A squash-merge is the usual316 reason `-d` refuses a branch whose content is fully merged**: `-d` judges by commit ancestry, and317 the squash replaced the branch's commits with one new-SHA commit, so ancestry is broken even318 though every line landed. That is not license to reach for `-D` reflexively — it means fall back319 to Step 1's *content* check (`git cherry`, superset diff) and only `-D` once that proves320 containment. Delete remote branches only after re-verifying the exact remote and repository321 visibility/ownership.322- **Independent clones: there is no safe git-level command — only `rm -rf`, which git cannot323 undo.** `git worktree remove` does not apply (it isn't a worktree) and refuses to help, so the324 usual "the tool will stop me if it's unsafe" backstop is absent here. Make the check explicit325 instead: gate the deletion on the backup actually existing, so a missing file aborts rather326 than being noticed afterwards.327328 ```bash329 for f in <backup>/<untracked-file> <backup>/uncommitted.diff <backup>/history.bundle; do330 [ -s "$f" ] || { echo "MISSING: $f — refusing to delete"; exit 1; }331 done332 rm -rf <clone-path>333 ```334335 Prefer deleting one clone at a time with its own verification over a loop across several — a336 glob that deletes five directories has five chances to be wrong and reports none of them.337338**Step 4 — after the delete, re-check by content, not by filename.** When a cleanup (or a batch of339squash-merges) is already done and the question becomes "did any of it drop work?", the naming-based340check that felt sufficient — `comm` over `git ls-tree` filenames, "every file is still on main" — is341not enough: identical filenames say nothing about identical *content*. A file the deleted branch and342the survivor both have can still differ line-for-line. Re-verify at blob level, and read the diff in343the right direction:344345```bash346git diff <survivor-ref> <deleted-or-merged-tip> # survivor first, the gone thing second347```348349Lines marked `-` are on the survivor but not the tip → the survivor is a **superset** (safe: it has350everything the tip had, and more). Lines marked `+` are on the tip but not the survivor → **candidate351loss** — run each through Step 1's ladder: is that symbol on the survivor under a different shape (a352rename or refactor, not a deletion)? A diff that is mostly `-` with a few `+` is the fingerprint of353"the survivor moved on and the deleted branch was an older version" — a merge that succeeded, not354work lost. Apply the same test to any preserved backup: byte-identical or survivor-superset is safe;355a line the survivor genuinely lacks anywhere is the one to escalate.356357**Recovery, if you regret it:** patches re-apply with `git apply`; the untracked tar extracts358in place; the bundle restores full history via `git fetch <file>.bundle <branch>:restored/<branch>`.359360## Scripts (execute these; they are non-destructive unless noted)361362| Script | Does | Mutates? |363|---|---|---|364| `scripts/git_find_all_checkouts.sh [root ...]` | Find every checkout of this repo on the machine — including independent clones invisible to `git worktree list` — and flag those holding uncommitted/untracked/unpushed work, plus how stale each one's cached remote refs are (`STALE_AFTER=<s>`, default 3600) | Nothing (read-only, no fetch) |365| `scripts/git_loss_audit.sh [remote]` | Refresh one remote, then report every worktree, local-only commit, stash, and dangler | Remote-tracking refs only |366| `scripts/git_preserve_danglers.sh [--patch-dir DIR]` | Pin danglers to `refs/dangling-backup/`, optional patches | Adds refs only (never deletes/gc) |367| `scripts/git_verify_branch_merged.sh <branch> [base]` | Refresh remotes, then give a content-level MERGED/UNMERGED verdict | Remote-tracking refs only |368| `scripts/git_export_before_drop.sh [--all-stashes] [--stash N] [--branch B] [--all-refs] [--out DIR]` | Export stashes plus selected branches or every current ref into verified bundles | Writes backup files only (never drops/deletes) |369370All five run from the repository root. They only ever `find`, `fetch`, `log`, `diff`, `show`,371`status`, `cat-file`, `rev-list`, `rev-parse`, `fsck`, `for-each-ref`, `remote get-url`,372`stash show`, `archive`, `bundle create/verify`, and (preserve only) `update-ref` — never373`checkout`, `reset`, `push`, `stash drop`, `branch -d`, or `gc`, so they are safe to run in a374dirty tree or alongside other agents. `git_find_all_checkouts.sh` additionally never fetches, so375it works offline and behind a proxy.376377## Troubleshooting378379- **An audit came back clean but the user still thinks something is missing** — believe them and380 suspect **scope, not thoroughness**. The in-repo instruments were probably all correct about the381 one directory they could see. Run Step 0 (`git_find_all_checkouts.sh`) before re-running anything382 you already ran; repeating a correctly-executed check in the wrong scope returns the same clean383 answer with more confidence behind it, which is worse than the first pass.384- **`git_find_all_checkouts.sh` finds nothing, but you're fairly sure another copy exists** — three385 likely causes, in order: (1) the copy lives outside the default roots (pass an explicit root such386 as `~`, and raise `DEPTH`); (2) it sits under a pruned path — the sweep skips `node_modules`,387 `.venv`, `vendor`, `.terraform`; (3) its `origin` points somewhere else entirely (a fork, or a388 path remote), so remote matching rejects it — check with `git -C <suspect> remote -v` and compare389 root commits by hand: `git rev-list --max-parents=0 HEAD`. A copy made by `cp -r` before the repo390 had any remote will only match on root commit.391- **`git_loss_audit.sh` reports dangling commits that look like old stashes** — expected after392 stash-heavy work. They're reflog-reachable now; pin them with `git_preserve_danglers.sh` if you393 want them past the gc window, then inspect with `git show <sha>` at leisure.394- **A branch shows huge "commits ahead" but you suspect it's merged** — trust395 `git_verify_branch_merged.sh` (content), not the count. See Mode C.396- **`git fetch` in a script hangs behind a proxy / offline** — loss detection still works on397 cached remote refs, because a stale cache can only over-report unpushed work. Merge and398 supersession verdicts (Mode C, Mode E) are the exception and genuinely need a fetch; without399 one, say so in the report rather than presenting the verdict as settled.400- **Your work looks unmerged, but the repository moved while you were working** — check the clock401 before you rescue anything: `git_find_all_checkouts.sh` prints when each checkout last fetched,402 and `git log --oneline <cached-base>..origin/main` after a fresh fetch shows what arrived403 meanwhile. A long session is the risk window — the base you compared against at the start can be404 many hours old by the end. Symptom to recognise: a change you know you committed appears absent405 upstream, so you prepare to re-ship it. Fetch first, then compare by content; if it did land,406 check whether anyone improved it before re-applying your version over theirs.407- **You're on a detached HEAD after checking out a commit** — that commit is safe as long as you408 `git switch -c <branch> HEAD` (or the reflog remembers it for ~90 days). Don't leave important409 new work on a detached HEAD across a `gc`.410- **Only one worktree remains after cleanup** — `git worktree list` always includes the primary411 repository checkout. Do not delete it merely to make the count zero; the goal is one maintained412 checkout, not no checkout.413- **`refs/dangling-backup/*` refs are cluttering things later** — once you've confirmed (Mode C)414 their content is on a remote, delete them with `git for-each-ref --format='%(refname)'415 refs/dangling-backup/ | xargs -n1 git update-ref -d`. Only after you've verified.416417## Next step418419After recovery/audit, if the repo also needs routine setup, safe commit/push, conflict handling,420or handoff hygiene, that's the `auto-repo-setup` skill's job (invoke `/auto-repo-setup`) — this421skill is the forensic/recovery layer, that one is the routine-workflow layer.