Clean Complete Branches
jtenniswood/espcontrol
Clean up completed Git branches and worktrees for this repository both locally and on GitHub.
Audits, preserves and recovers local Git state before cleanup: unpushed or wrong-branch commits, dirty worktrees, duplicate clones, stashes, dangling commits, squash-merge uncertainty.
$ npx skills add daymade/claude-code-skills --skill git-safety-net -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install daymade/claude-code-skills git-safety-net --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/daymade/claude-code-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/git-safety-net .claude/skills/git-safety-net && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "git-safety-net" agent skill from https://github.com/daymade/claude-code-skills/tree/main/git-safety-net into .claude/skills/git-safety-net/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "git-safety-net", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/daymade/claude-code-skills/tree/main/git-safety-netType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add daymade/claude-code-skills --skill git-safety-net -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install daymade/claude-code-skills git-safety-net --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/daymade/claude-code-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/git-safety-net .agents/skills/git-safety-net && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "git-safety-net" agent skill from https://github.com/daymade/claude-code-skills/tree/main/git-safety-net into .agents/skills/git-safety-net/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "git-safety-net", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add daymade/claude-code-skills --skill git-safety-net -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install daymade/claude-code-skills git-safety-net --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/daymade/claude-code-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/git-safety-net .cursor/skills/git-safety-net && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "git-safety-net" agent skill from https://github.com/daymade/claude-code-skills/tree/main/git-safety-net into .cursor/skills/git-safety-net/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "git-safety-net", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/daymade/claude-code-skills.git --path git-safety-net--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add daymade/claude-code-skills --skill git-safety-net -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install daymade/claude-code-skills git-safety-net --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/daymade/claude-code-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/git-safety-net .gemini/skills/git-safety-net && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "git-safety-net" agent skill from https://github.com/daymade/claude-code-skills/tree/main/git-safety-net into .gemini/skills/git-safety-net/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "git-safety-net", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install daymade/claude-code-skills git-safety-netInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add daymade/claude-code-skills --skill git-safety-net -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/daymade/claude-code-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/git-safety-net .github/skills/git-safety-net && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "git-safety-net" agent skill from https://github.com/daymade/claude-code-skills/tree/main/git-safety-net into .github/skills/git-safety-net/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "git-safety-net", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add daymade/claude-code-skills --skill git-safety-net -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install daymade/claude-code-skills git-safety-net --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/daymade/claude-code-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/git-safety-net .opencode/skills/git-safety-net && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "git-safety-net" agent skill from https://github.com/daymade/claude-code-skills/tree/main/git-safety-net into .opencode/skills/git-safety-net/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "git-safety-net", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
git-safety-netAudits, preserves and recovers local Git state before cleanup: unpushed or wrong-branch commits, dirty worktrees, duplicate clones, stashes, dangling commits, squash-merge uncertainty.
Git Safety Net is an agent skill from daymade/claude-code-skills. Audits, preserves and recovers local Git state before cleanup: unpushed or wrong-branch commits, dirty worktrees, duplicate clones, stashes, dangling commits, squash-merge uncertainty. Use when the user fears lost work, wants to recover a commit, asks if a branch is actually merged, asks whether a branch, worktree or clone is safe to delete, or wants one clean main (误删分支 / 还有没有丢的东西). Not for GitHub PR operations.
Its SKILL.md is about 17k tokens, which your agent loads only when the skill is triggered. The skill folder holds 29 other files, including scripts and reference files (for example `agents/openai.yaml`, `evals/evals.json` and `references/merge_verification.md`).
It sits in Development, covering Git worktrees and Git workflow. It works with Git and GitHub. The repository describes itself as: Professional Claude Code skills marketplace featuring production-ready skills for enhanced development workflows. The licence is MIT.
6 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 91bed2b. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 9 files in scripts/ (Shell and Python, from the files we listed), which the agent can run.
Shell commands in SKILL.md call:
gitghuvbundleFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use git, gh and uv, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Git Safety Net loads about 17k tokens when it runs, and up to ~47k if it reads all its reference files. Until then it costs about 108 tokens; SKILL.md has 8,953 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from daymade/claude-code-skills at commit 91bed2b, republished under its MIT licence (© daymade). 8,953 words, ~16,618 tokens.
.claude/skills/git-safety-net/SKILL.md (or your agent's skills folder). This skill also uses 24 other files; get the full folder from GitHub.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.
Before Mode B/E or any command that writes a ref or backup, state the following in the conversation (do not create another file):
Then enforce these boundaries:
| 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 the smallest read-only probe that selects a mode. Use Mode B Step 0's machine-wide discovery only when the outcome is an exhaustive loss audit or the target checkout is unknown. A named repository/branch/worktree task stays named; findings outside that target are report-only until the user expands the authorized targets.
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 only
for an exhaustive audit or unknown target; otherwise audit the named target. 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.
Scope has a third axis: the REF SET itself moves. A branch inventory and a verified bundle
prove what existed at one instant; they do not authorize deletion five minutes later. Immediately
before deleting, re-enumerate local refs and hosting-service branches, then require every target
ref to still equal the object recorded in the bundle. A new branch, a moved tip, or a new parallel
PR reopens classification and requires a new bundle. Do not delete against a stale inventory.
Scope has a fourth axis: OWNERSHIP. Repository visibility does not make every visible object
part of this task. Partition discovered refs/worktrees/PRs into change-authorized, inspect-only,
and explicitly excluded sets before acting; compute cleanup success over the authorized set.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 in the named checkout, and in additional Step 0 checkouts only after each is
explicitly change-authorized. Run this repository-wide script only when every surface it
enumerates—linked worktrees, local refs/tags, stashes, and dangling commits—is inside the
declared evidence scope. It has no exclusion flags. Otherwise limit the claim to the authorized
checkout/ref and use its own status, HEAD, upstream/remote identity, and git log HEAD --not --remotes as scoped evidence; report the other surfaces as not audited.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.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 those formats — 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.
When the backup is a branch pushed into an already-existing repository rather than this one's
own remote, verify shared history before the push (Mode B Step 2) — a similar name proves
nothing.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.A commit/branch/stash that "disappeared" is almost always still in the object store for ~90 days —
why that is true, and what ends it, is references/recovery_playbook.md § Mental model: nothing is gone
until gc runs. The ladder there is indexed by symptom, so go straight to your rung —
§ Ladder step 1 — git reflog (where most recoveries end),
§ Ladder step 2 — dropped stashes,
§ Ladder step 3 — detached-HEAD work,
§ Ladder step 4 — git fsck for true orphans. 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 workIf reflog doesn't show it, fall through by symptom rather than reaching for fsck first: a
dropped stash has its own recovery route (references/recovery_playbook.md § Ladder step 2), work
abandoned on a detached HEAD has another (§ Ladder step 3), and only a true orphan — from a rebase, say —
needs git fsck --dangling (§ Ladder step 4).
Step 0 — establish the evidence scope (rule 1). What "at risk" covers, and the checkout kinds that hide it, are references/recovery_playbook.md § The authoritative "is anything at risk" check and § Linked worktrees and detached worktree HEADs. Run machine-wide checkout discovery only when the Outcome contract calls for an exhaustive audit or the target checkout is unknown. For a named target, record that checkout and continue to Step 1 without turning an unrelated clone into work. When exhaustive discovery is warranted, 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 otherIt 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.
For an inspect-only checkout, stop at discovery: Step 1 fetches and changes its remote-tracking
refs. Run Step 1 only after that checkout is change-authorized; apply Step 2 only to authorized
items. A "nothing at risk" claim covers only the checkouts actually audited.
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:
When every worktree/ref/tag/stash/dangler the script enumerates is inside the declared evidence scope:
scripts/git_loss_audit.sh # defaults to remote "origin"; pass a remote name to overrideWhen any surface listed above is excluded, skip that script and collect only checkout/ref-scoped evidence:
git status --porcelain=v1 --untracked-files=all
git rev-parse HEAD
git log --oneline HEAD --not --remotes
git ls-remote --exit-code <remote> refs/heads/<branch> # 0 present · 2 absent · 128 probe failedBoth the --exit-code and the fully-qualified refs/heads/ prefix are load-bearing here, not
style: a bare git ls-remote <remote> <branch> reports an unreachable remote and an absent ref
identically (each prints nothing), and a bare branch name also matches a same-named tag. The trap
is measured in both directions under Troubleshooting § "A 'did that branch get deleted?' probe
says it still exists".
For the full audit, expected output is 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. The scoped path proves only the authorized checkout/ref; it says nothing about excluded worktrees, other local refs, stashes, or danglers, which must remain listed as not audited.
Step 2 — preserve only what the next authorized destructive action threatens (additive, gc-proof). A finding alone does not need a backup. If deletion, gc, or history rewriting can make a reported commit unreachable, preserve that exact commit before the action. Use the whole-set helper only when every reported dangler is actually in the authorized target set:
scripts/git_preserve_danglers.sh --patch-dir ~/git-danglers # pin + export patchesWhy pinning survives gc, and the targeted single-ref form when the whole set is not in scope:
references/recovery_playbook.md § Preserve: pin authorized danglers so gc can never take them.
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
§ Triple-backup a critical commit.
Before pushing that preservation branch into an already-existing repository, verify shared
history first — a similar name is not evidence of the right repository. Fetch the candidate
repository's default branch (git fetch <candidate-url> <default-branch>) and run
git merge-base <that-default-branch> <ref>; a local
merge-base exiting 1, or the hosting service's own compare view reporting no common ancestor,
means the two share no history and the target is a different project. Push the branch back to this
work's own remote first; if none exists, create a new repository; if neither applies or the correct
home is unclear, ask the user — but never push anyway because the name matched. Keep the merge-base
check as the test whenever the right home is not obvious: only a repository that already shares
this work's history can host the branch without being a different project. Real incident: a
deployment source's backup branch was pushed to an unrelated private repository chosen by name
resemblance alone. If a push already landed and a
later readback finds no common ancestor, treat the branch as misplaced: move its content to the
correct home, then delete it from the wrong one, rather than leaving it there as "already backed
up somewhere."
Untracked files need a different tool — plain copying (rule 4). Put <backup> outside the
target repository and every checkout being retired. Everything above moves git
objects; a file git was never told about is not one. Preserve those explicitly, and keep the
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 restoresWrite 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.
The trap: a stale branch shows "173 commits ahead of main" yet every line is already on main (squash-merge artifact) — the mechanism is references/merge_verification.md § Why commit counts lie. Never conclude "unmerged" from counts. Per-branch content check (procedure and output reading: § Per-branch verdict procedure):
scripts/git_verify_branch_merged.sh <branch> [<base>] # base defaults to origin/mainThis 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. Because fetch moves remote-tracking refs, run it only after existing coordination has
quiesced every checkout writer and transferred exclusive ownership. If that cannot happen, stay
read-only and report that the merge verdict is unavailable. If the fetch itself fails after
ownership transfer, the script 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 — the sole writer must refresh authority first, and two-dot vs three-dot
answers different questions (references/merge_verification.md § Pick the diff FORM from the question
you're asking). Signals that may inform a human but must never auto-decide are fenced off in
§ Manual-only investigation hints.
It reports MERGED (ancestor) or MERGED (content contained) — content-safe for a separately
authorized Mode E deletion gate — 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 reports content containment 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
(references/merge_verification.md
§ Why safety-biased). Why the trial merge is sound rather than a
heuristic, and why --find-object/blob comparison is not: § The sound content check. When base has
changed the SAME lines again since a squash-merge (not just any later edit — an unrelated file or
unrelated lines still leave the current-base trial merge sound) and it now conflicts instead of
reproducing base's tree, --merge-commit <sha> proves containment at that historical merge commit
instead — a different, narrower question than "does base have it now" (§ The
historical-merge-commit rung). For a whole
repo of branches against one frozen base, scripts/git_classify_refs.sh --base <sha> [--pr-map <json>] runs this same ladder — including the merge-commit rung, via --pr-map — in one offline
pass instead of one invocation per branch. For a whole
repo of branches, the read-only fan-out pattern — one agent per batch, each told to falsify
"everything is merged," every finding independently re-checked — is
§ Adversarial multi-agent verification, with the constraints those agents must be given in
§ Rules for the verification agents.
The habits that keep a branch tangle from ever stranding work:
references/prevention_practices.md. Each bullet below carries the § name of its full
treatment there — follow the one that matches your situation rather than reading the whole file.
The load-bearing few:
HEAD, and index but still share refs, stashes, object storage, config,
and hooks, and do not copy ignored dependencies. When approved, a linked worktree is safer than
an invisible independent clone but remains a separately audited retirement target.
(§ Audit every authorized worktree before retirement.)HEAD,
and fresh remote tip; require them to equal the handoff SHA. Direct merges name that SHA, not the
branch. Hosted merges use an expected-head-SHA precondition when available, or an immediately
preceding hosted head readback that must still equal the handoff SHA. Every session-owned byte
must be in that remote-backed commit, and every residual path must be enumerated and attributed.git branch --show-current) — a fix committed
onto the wrong feature branch is invisible to its real PR and easy to lose on cleanup.
(§ Confirm the branch before every commit — including why removal from the wrong branch waits
for Mode E.)reset --hard, merge, and rebase all act on whatever is checked
out at the instant they run. Use checkout-independent forms for ref repair when they match the
authorized outcome: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>reset --hard origin/main issued while another session still owned the checkout
landed on that session's feature branch and moved it back two commits. The correct first action is
to stop and transfer ownership; once transferred, an explicitly targeted ref repair avoids making
checkout position part of the operation.HEAD sits on
your branch becomes a parent of yours and ships inside your PR. git branch --show-current names
your branch, the tree is clean, and git diff --cached --name-status shows exactly your paths —
all true, all blind, because their work left the index the moment they committed. It appears only
in the branch's cumulative range against the base you branched from. Detection is read-only, so
run it before every push and before opening any PR:base=<the base SHA you recorded when you created the branch>
git rev-parse --verify "$base^{commit}" # must print a SHA — see below before trusting the rest
git log --oneline "$base"..HEAD # every commit here must be yours
git diff --name-only "$base" HEAD # every path here must be yours$base turns "$base"..HEAD into HEAD..HEAD:
no output at exit 0, indistinguishable from "no foreign commits". That silent case is the one the
guard exists for; § A foreign commit adopted onto your branch has it and the louder one measured.
And "yours" is not derivable from Git: in a shared checkout both sessions write the same
author and committer, so no flag separates them. It comes from the SHAs you recorded as you
committed. If you cannot say which commits are yours, stop and ask — the repair deletes a commit,
so a guess here is the loss this skill exists to prevent.
Record that base SHA when you branch — deriving it later reads a cached remote ref, and the fetch
that would refresh it is itself ownership-gated § A foreign commit adopted onto your branch.
A foreign commit in that range is evidence another writer was in this checkout, so repair is
not yours to start. Stop; the ownership rules above apply unchanged. Once ownership has
transferred, repair is a history rewrite of your branch — git rebase --onto checks out the
branch it rewrites — so it runs the existing sequence rather than a shortcut: the applicable Mode B
evidence path, then git branch backup/pre-rewrite <your-branch> (Snapshot before any history rewrite
— this is what makes the rebase reversible), then git branch rescue/foreign-<short-sha> <foreign-sha>
(this preserves their work, a separate obligation and a different ref), then
git rebase --onto "<foreign-sha>^" "<foreign-sha>" <your-branch> — onto the foreign commit's
parent, never onto the base, because --onto "$base" discards everything before the foreign
commit, your own earlier commits included, and exits 0. Then re-run the detection above: the
rebase's exit code does not tell you whether it took something of yours with it. Retiring either
ref afterwards requires Mode C/E deletion-grade evidence, not a guess. Full procedure,
and why the two obvious "did their work survive?" probes return the wrong answer, in A foreign
commit adopted onto your branch in
references/prevention_practices.md. Real incident: a
sibling session committed while HEAD sat on a freshly created branch; the PR carried that
session's in-progress work, and the only signal was a repo validator reporting two changed
components when the author had touched one.switch, add, reset, create commits with a temporary index, update refs, or push. Use
the repository's coordination system to quiesce that writer and transfer exclusive ownership; if
none exists, report the gap and preserve the current evidence. Once you are the sole writer, an
object-store-only commit can keep attributable foreign WIP out of the shared index and working
tree. Freeze every candidate as the exact Git entry tuple (mode, object ID, path) — bytes alone
are insufficient because 100755, 120000, and 160000 carry executable, symlink, and gitlink
behavior. The safest source is an immutable candidate commit:candidate_ref=<immutable-candidate-commit-oid>
candidate_path=path/to/file
candidate_entry=$(git ls-tree "$candidate_ref" -- "$candidate_path")
candidate_mode=$(printf '%s\n' "$candidate_entry" | awk 'NR == 1 { print $1 }')
candidate_oid=$(printf '%s\n' "$candidate_entry" | awk 'NR == 1 { print $3 }')
test -n "$candidate_mode" && test -n "$candidate_oid" || exit 1
candidate_index=$(mktemp /tmp/git_safety_candidate_index.XXXXXX)
export GIT_INDEX_FILE="$candidate_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 "$candidate_mode,$candidate_oid,$candidate_path"
tree=$(git write-tree)
commit=$(git commit-tree "$tree" -p origin/main -m "…") # HEAD does not move
unset GIT_INDEX_FILE
rm "$candidate_index"
git push origin "$commit":refs/heads/<branch> # open the PR from here100755 when executable, otherwise 100644) and hash its bytes; fail instead of
applying that route to a symlink or submodule. For those entry types, first freeze an immutable
candidate commit and copy its mode/object tuple as above. Never source an entry from a shared path
that another session is editing. 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. It is a
sole-writer preservation technique, not permission to mutate while someone else owns the repo.
commit-tree
does not run the normal git commit hook path: execute the repository's exact pre-commit/security
gates against the candidate before push, and still let pre-push run. Use it only after ownership
transfer, when preserved foreign WIP makes checkout switching or shared-index staging unsuitable.git commit snapshots the whole index, not just what you staged — and a commit that
bypassed the index leaves a trap in it. Moving the current branch without updating the
shared index advances HEAD while the index stays on its old baseline — via commit-tree +
update-ref on that branch, or a git commit through a temporary GIT_INDEX_FILE. (The
sole-writer push-to-another-branch path above moves no local ref, so it leaves no drift.) Every
file the new commit introduced then shows as a staged deletion (git status prints D
lines plus matching ?? untracked entries). git commit -- <path> neither creates nor repairs
this drift — it only updates its own paths. The drift detonates on anyone's next bare
git commit: that commit snapshots the entire index, turning the phantom deletions real —
delivered files vanish from HEAD while the working tree looks untouched. Real incident:
a 24-file delivered directory sat in that window after a temporary-index commit; one bare commit
by a parallel session would have deleted it from the branch tip, and the only sign anywhere was
D lines in git status. Two obligations follow. Whoever advanced the branch past the index
re-syncs immediately — git diff --cached --name-status, then git restore --staged -- <the paths the commit touched> until those paths no longer appear in the diff (a parallel
session's own staged entries are theirs, not yours to clear). And before any bare commit on a
shared tree, read that same diff as your blast radius — every entry, D lines included, must
be one you intended; an entry you don't recognize means stop, not commit.
(§ Commit-scope hygiene.)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 from the Outcome contract. For an exhaustive audit or unknown target, run checkout discovery;
for one named worktree/branch, stay in its owning repository. Run git_loss_audit.sh only when all
worktrees/refs/tags/stashes/danglers it enumerates are inside evidence scope; treat inspect-only
objects as report-only, keep explicitly excluded collaborator resources out of both the retirement
plan and its terminal counts, then retire only the named targets. If any enumerated surface is
excluded, do not run the full loss audit or an --all-refs export; use checkout/ref-scoped checks
and targeted exports instead:
Step 1 — classify each leftover: live WIP, or superseded draft? Evidence ladder, strongest first:
scripts/git_verify_branch_merged.sh. An ancestor/content-contained verdict is deletion-grade
evidence. If it returns NEEDS REVIEW, continue down this ladder; do not convert uncertainty to
MERGED with a weaker heuristic. For many leftovers against one frozen base,
scripts/git_classify_refs.sh runs this exact rung across every branch in one offline pass.git cherry <base> <branch> is a hint, not a verdict. A - proves that one patch-id is
upstream; a + does not prove missing work because squash merges deliberately create a new
patch-id. Never rescue or delete a whole branch from this output alone.+ 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.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 the rungs (including the squash-artifact and absorbed-into-refactor cases): references/merge_verification.md § Supersession triage.
Step 2 — after deletion authority exists and immediately before deletion, preserve exactly what that deletion threatens:
# Targeted branch cleanup: prefer the narrow export.
scripts/git_export_before_drop.sh --branch <branch> --out <external-backup-dir>
# Pin only an authorized dangling SHA; leave unrelated danglers report-only.
git update-ref refs/dangling-backup/<sha> <sha>
# Full ref topology: only when every captured ref is explicitly authorized.
scripts/git_export_before_drop.sh --all-refs --out <external-backup-dir>
scripts/git_export_before_drop.sh --verify-current <external-backup-dir>/all-refs.bundleThe targeted update-ref reaches only the authorized dangling commit. If every reported dangler is
in scope, the whole-set git_preserve_danglers.sh may replace it. Prefer repeated --branch options
for named branch/worktree retirement. --all-refs captures branches, tags, stashes, hidden backup
refs, and linked-worktree HEAD refs, so it is valid only when that whole captured set is authorized;
add --all-stashes only when stashes are also deletion targets. --verify-current is the final
compare-and-swap gate: it exits 1 if any recorded ref moved or disappeared. Refresh remote authority
before it, and rebuild the bundle on any mismatch. Keep backups outside the repository; never turn
one branch into a repo export.
For a multi-branch "only one main" cleanup while other sessions may still commit or open PRs, read references/merge_verification.md § Converging many branches to one main through single-writer windows before Step 3. While another writer is active, that route is read-only: fetch, object/ref creation, bundle export, push/PR, and deletion wait for existing coordination to prove quiescence and transfer exclusive ownership through final readback. The reference adds the moving-ref inventory, dirty-WIP preservation, immutable-candidate, duplicate-PR, and final branch-count gates that a single-branch retirement does not need.
Step 3 — destroy, in the safe order:
drop stash@{2} before stash@{1}) — indices
shift as you drop, and top-down keeps every number meaning what your backup filenames say.git -C <path> status --porcelain=v1 --untracked-files=all,
then inventory ignored paths separately with --ignored. A normal clean status hides !!
files, and no bundle can preserve them; copy out anything not proven reproducible, preserve its
relative path, and verify it against a recorded pre-removal content hash.
Record the exact HEAD, prove it contained/superseded against a freshly fetched base, export its
branch or collision-checked recovery ref into a verified targeted bundle, and obtain
current-session deletion authority. As the final pre-remove gate, re-run the empty status and
the complete ignored inventory. Require exact equality with the frozen
pre-removal manifest for every ignored path, entry type, file hash, and symlink target, and
re-verify each preserved source and backup copy; any difference aborts. Removal must be the next
operation. Remove only with
git worktree remove <absolute-path> without --force. Afterwards prove the path and
registration are gone while the recorded HEAD still resolves through the kept branch/base or the
verified bundle and every copied ignored item still matches its recorded hash. Never remove the
primary/current checkout; retire its branch only as a separate,
separately authorized action. Follow
references/merge_verification.md § Worktree retirement.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. -d also judges "merged" relative to the checkout's CURRENT HEAD (or the
branch's configured upstream), not relative to the base you have in mind — a checkout sitting
on a stale branch makes -d refuse a branch that genuinely is an ancestor of the intended base,
which reads exactly like the squash case above but has a different fix: don't reach for -D,
confirm ancestry directly against the intended base with
git merge-base --is-ancestor <tip> <base> and switch/target correctly instead. Delete remote
branches only after re-verifying the exact remote and repository visibility/ownership.-D, check one thing neither containment nor ancestry covers: no open PR may have this
branch as head. A branch with zero unique content can still be an open PR's head, and deleting
it takes that PR's head with it — refs/pull/<N>/head usually still serves the objects, but that
is recovery by luck, not by design (the ladder is in
references/recovery_playbook.md § Ladder step 4). On
GitHub, confirm by PR number, not by branch name: gh pr list --head <branch> has returned an
empty result for a branch whose pull request already existed, and "a branch-deletion decision built
on the empty result would have been wrong" (measured — the record lives in
references/merge_verification.md § The historical-merge-commit
rung, under "On GitHub, do that confirmation by PR number"). Use
gh pr view <number> --json headRefOid,state,mergeCommit, or the REST equivalent
gh api repos/<owner>/<repo>/pulls/<number>; when the number is not yet known, REST search still
beats pr list:
gh api "repos/<owner>/<repo>/pulls?state=all&head=<owner>:<branch>". On a non-GitHub remote, use
the platform's own PR lookup — a branch-name grep is not a substitute. Do it immediately before
the delete, never from an inventory taken earlier: refs move, and "a PR opened after your inventory"
is exactly the case this catches. Measured 2026-09-22 — a peer relayed "that branch maps to a closed
PR, safe to delete" into a dispatch prompt, and the executing agent's own pre-delete recheck found
it was the head of a newly opened PR.git push <remote> --delete <branch> exits
1 with remote ref does not exist — the end state you wanted, reported as an error. And the
obvious readback is the one that breaks: git ls-remote <remote> <branch> printing nothing is not
proof the branch is gone, because an unreachable remote prints nothing either. Use
git ls-remote --exit-code <remote> refs/heads/<branch> and read the code — 0 present, 2 absent,
128 the probe itself failed, where 128 means the branch's fate is unknown and nothing may be
retired on that reading. Both traps are measured in both directions under Troubleshooting §
"A 'did that branch get deleted?' probe says it still exists", which also covers why the ref must
be fully qualified.objects/info/alternates dependency created by git clone --shared. Run
scripts/git_prepare_clone_retirement.sh --clone <absolute-clone> --survivor <absolute-kept-checkout> --out <new-external-backup-dir>;
it refuses those hidden loss states, every clone-only unreachable Git object, partial/promisor
clones, attached linked worktrees, local submodule repositories, known Git LFS/annex object stores,
tracked content filters, and repository-local config/hook indirection that the recovery archive
cannot resolve safely. It disables repository fsmonitor execution, freezes ref tips plus
symbolic-ref topology and metadata file types/modes, then creates a self-contained all-refs bundle
and a content-bound receipt. Finish every preliminary Git probe, freeze the absent quarantine
target, run process occupancy by itself, then run --verify-current <backup-dir> as the final
probe with the authorized no-clobber quarantine move as the next operation. This order keeps
lsof from observing a sibling Git process without opening a larger post-verification gap. Prove
old-path absence + new-path presence; permanent deletion is a separate explicit decision. Full
READ-DO sequence and --shared boundary:
references/merge_verification.md § Independent clone retirement.
This retirement occupancy check is the second of the Skill's three lsof uses — the shared-file
writer probe in references/prevention_practices.md and references/merge_verification.md's
clone-occupancy probe share the same read-only, stop-on-any-genuine-writer rule; change the
criterion in one, change it in the others.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 secondLines 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>.
Step 5 — close the temporary recovery-artifact lifecycle only when it is separately authorized. First finish Step 4 and prove the maintained survivor contains or intentionally supersedes every payload. Then follow references/merge_verification.md § Retire task-owned temporary recovery artifacts. A general request to converge onto one main branch does not authorize deleting the external backup. Keep any artifact that remains the only copy of work, contains an unresolved payload, mixes cleared and unresolved payloads, or was not created or explicitly adopted by this task. After an authorized retirement, independently prove every exact artifact path is absent; the deletion command's receipt is not the postcondition.
Every scripts/... path below is relative to this Skill's bundle root, not a command promised on
PATH or in the target repository. Resolve the loaded Skill directory and invoke the bundled path;
never tell a user to run bare git_verify_branch_merged.sh unless command -v actually finds it.
| 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 uncommitted/untracked/unpushed work, remote-cache age, and borrowed alternates object stores | Nothing (read-only, no fetch) |
scripts/git_hosted_vs_cached.sh [remote] | Ask the hosting service directly (git ls-remote --heads) and diff it against this checkout's cached refs/remotes/<remote>/*: same / different-SHA / hosted-only / cached-only (prune candidate). A failed ls-remote exits 3 and never presents the cache as hosted truth | Nothing (read-only, no fetch — the one authoritative query is ls-remote, not fetch) |
scripts/git_loss_audit.sh [remote] | Refresh one remote, then report every worktree, local ref/tag, stash, and dangler; no exclusions, so the whole evidence surface must be in scope | Remote-tracking refs only |
scripts/git_preserve_danglers.sh [--patch-dir DIR] | Pin every dangling commit to refs/dangling-backup/, optional patches; whole-set only | Adds refs only (never deletes/gc) |
scripts/git_verify_branch_merged.sh <branch> [base] [--no-fetch] [--base REF] [--merge-commit SHA] | Give a content-level MERGED/UNMERGED verdict for one branch. --merge-commit adds a rung that proves containment at a historical merge commit when base later changed the same lines and the CURRENT base can no longer prove it; the verdict also names the current-base shape (conflict = routine, clean-but-changes = regression candidate). --no-fetch/--base support offline and flag-driven batch use | Remote-tracking refs only (skipped entirely with --no-fetch) |
scripts/git_classify_refs.sh --base SHA [--pr-map JSON] [--all-namespaces] | Batch version of the above: classify every local/remote-tracking branch against one frozen base in a single offline pass (ancestor / content-contained / needs-review, with file-count-changed); --pr-map overlays the --merge-commit rung from a hosting platform's PR export | Nothing (read-only, no fetch — pin the base with git rev-parse first) |
scripts/git_align_checkout.sh --preview --target SHA / --apply --target SHA --source-worktree PATH --backup-manifest FILE | Preview or materialize a checkout's content up to a target commit without a throwaway snapshot branch and without moving HEAD; --apply only overwrites a path once its current content is proven reproducible (target's own history, or a manifest sha256) | --preview: nothing. --apply: working-tree files only for paths it proves safe — never the real index, never HEAD, never a delete |
scripts/git_export_before_drop.sh [export options] | Export stashes plus selected branches or every current ref into verified bundles | Writes backup files only (never drops/deletes) |
scripts/git_export_before_drop.sh --verify-current BUNDLE | Fail if any bundled ref moved or disappeared since export | Nothing (read-only) |
scripts/git_prepare_clone_retirement.sh --clone PATH --survivor PATH --out DIR | Refuse hidden/unhandled clone state, then freeze every ref tip, symbolic-ref target, reflog identity, and scoped config/hooks/info metadata into a self-contained recovery set; after freezing an absent no-clobber destination and process occupancy, --verify-current DIR is the final probe and the move must be the next operation | Writes only the new external backup directory; disables lazy fetch/fsmonitor and refuses tracked content filters; never moves/deletes or changes refs |
These helpers run from the repository root. They use read-only enumeration/configuration commands such as
find, config, symbolic-ref, submodule status, status, cat-file, rev-list, rev-parse,
fsck, for-each-ref, and remote get-url; plus scoped fetch, archive, bundle create/verify,
metadata hashing/archive, and (preserve only) update-ref where each script's table row says so.
Only git_find_all_checkouts.sh is repository-read-only and safe beside read-only agents; it never
fetches, so it also works offline and behind a proxy. Any helper that fetches, writes backup state,
or adds refs runs only after existing coordination transfers exclusive writer ownership. None of
the helpers authorizes checkout, reset, push, stash drop, branch -d, or gc.
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 one authorized SHA with targeted update-ref,
or use git_preserve_danglers.sh only when every reported dangler is in scope, then inspect with
git show <sha> at leisure.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.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.origin/<branch> makes work the remote already has
read as unique, which is the double-apply this entry opened with. On a contended checkout you
may not fetch, and a verdict read off a cached ref is not a verdict — report it as unavailable.
And
git merge-base --is-ancestor <your-sha> origin/<branch> answers only where the merge preserved
your commit — a squash or rebase merge re-writes it, so exit 1 there means "not this object", not
"not landed", and acting on it produces exactly the double-apply this entry warns about (measured
in one repository on one day: one merged PR's commit was an ancestor, another's was not, and both
had landed). It has a third exit code too: 128 when <your-sha> is not a commit this
repository has — which is what you get for a hosted merge that created its own object, and it is
not the same answer as 1. The probe that survives either merge strategy is content, with its
control line:git show origin/<branch>:<path> | grep -cF '<a string only your version contains>'
git show origin/<branch>:<path> | grep -cF 'string-that-cannot-exist' # must be 0, or the probe is brokenorigin/main advanced
twice — once for this author's merge, once for a parallel session's.git ls-remote <remote> <ref> > f followed by [ -s f ] is wrong in
both directions and which one you get depends on a redirection detail. Measured against an
unreachable remote: bare > f leaves the file at 0 bytes (Git writes the failure to stderr),
so the test reports "already gone" for a branch whose fate is unknown — and you stop preserving
it. Merge the streams (> f 2>&1, &> f) and the same failure writes 155 bytes, so the test
reports "still there" for a branch that is long gone. Use the exit code the command has for
exactly this: git ls-remote --exit-code <remote> refs/heads/<branch> returns 0 when the ref
matched, 2 when it did not, and anything else (128 for an unreachable remote) means the
probe itself failed — outcomes the file-size test collapses incorrectly, differently each time.
On 128 the branch's fate is unknown, which is not the same as gone: keep whatever preserves it,
retire nothing on this reading, and either retry once the remote is reachable or hand the question
to a human. An unreachable remote is a reason to wait, never a reason to clean up.
Pass the fully-qualified ref: ls-remote matches on the tail of the name across all
namespaces, so a bare branch name also matches a same-named tag and returns 0 for a branch that
was deleted (measured — a common shape after a release tags its branch name). Same trap in the working tree: [ -e <path> ] cannot tell a tracked-and-clean
file from an untracked collision. Ask Git instead — git cat-file -e <branch>:<path>, where 0 means the
branch has it. Do not read non-zero as "it does not": 128 also covers a mistyped branch name and a
path that traverses a tracked symlink, and Git distinguishes them only in the stderr text — so
read that text rather than the code alone, or you have rebuilt the very defect this entry opened
with.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. Recovering work already abandoned there:
references/recovery_playbook.md § Ladder step 3 — detached-HEAD work. If ~90 days is too short
for how this repo is used, widen it once: that reference's § Widen the safety window (config),
and references/prevention_practices.md § Set a wider reflog safety
window once.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.git cherry <upstream> <branch>. All - lines means every local-only commit is patch-identical
to one already upstream (git cherry compares patch-ids, so the SHAs need not match). The
typical cause: you committed on a branch whose remote-tracking ref was stale, and the same change
was later re-made and pushed. When every line is -, fast-forward the branch to the upstream —
the duplicate objects stay recoverable in the object store. Any + line is real local-only work;
treat it as an ordinary divergence instead (and remember the Step 1 caution cuts the other way:
after a squash merge a + does not prove missing work, so read git cherry as a hint, not a
verdict — all-- is the only reading that licenses the fast-forward).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.
© daymade, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 24 other files (scripts, references) in git-safety-net of daymade/claude-code-skills.
Open the folder on GitHubat commit 91bed2b
Git Safety Net next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Git Safety Net this skilldaymade/claude-code-skills | 1.4k | — | ~17k | Automated safety check: Pass | MIT | |
| Clean Complete Branchesjtenniswood/espcontrol | 1.1k | — | ~820 | Automated safety check: Pass | Custom licence | |
| Project Session ManagerYeachan-Heo/oh-my-claudecode | 40k | — | ~4k | Automated safety check: Pass | MIT | |
| Conventional Gitsamber/cc-skills | 228 | — | ~1.8k | Automated safety check: Pass | MIT | |
| Finishing a Development Branchobra/superpowers | 296k | 5 repos | ~1.9k | Automated safety check: Pass | MIT | |
| Contributor-First PR MergeHKUDS/OpenHarness | 16k | 1 repos | ~847 | Automated safety check: Pass | MIT |
jtenniswood/espcontrol
Clean up completed Git branches and worktrees for this repository both locally and on GitHub.
Yeachan-Heo/oh-my-claudecode
Creates isolated git worktrees, with optional tmux sessions, for PR reviews, issue fixes and feature work across projects and repositories.
samber/cc-skills
Conventional Commits v1.0.0 branch naming, worktree naming, and commit message standards for GitHub and GitLab projects.
obra/superpowers
Walks the last step of a branch: confirm tests pass, detect the git environment, ask how to integrate, carry out your choice and clean up the worktree.
HKUDS/OpenHarness
Merges external GitHub pull requests while keeping the original author credited, and fixes conflicts after the merge instead of rewriting the contribution.
cline/cline
Opens a GitHub pull request from your current branch with the gh CLI, after reviewing the commits and diff and gathering the details the PR needs.
daymade/claude-code-skills
This skill should be used when comparing two videos to analyze compression results or quality differences.
daymade/claude-code-skills
Generates professional animated CLI demos as GIFs using VHS terminal recordings.
daymade/claude-code-skills
Converts DOCX/PDF/PPTX and saved HTML/HTM to high-quality Markdown with automatic post-processing.
daymade/claude-code-skills
Generates several distinct, clickable HTML interaction prototypes for one product surface into a Design Board and collects selection/remix feedback before implementation.
daymade/claude-code-skills
Diagnoses and repairs repository setup and guarded Git workflows for Claude Code or Codex — environment repair, startup sync, hook auditing, collaborator handoff.
daymade/claude-code-skills
Pulls Bigdata.com (RavenPack) financial and news data via the official bigdata-client SDK and /v1/ REST endpoints — structured financials, prices, analyst estimates, entity-sentiment series…
Categories
Audits, preserves and recovers local Git state before cleanup: unpushed or wrong-branch commits, dirty worktrees, duplicate clones, stashes, dangling commits, squash-merge uncertainty. Git Safety Net is an agent skill from daymade/claude-code-skills. Audits, preserves and recovers local Git state before cleanup: unpushed or wrong-branch commits, dirty worktrees, duplicate clones, stashes, dangling commits, squash-merge uncertainty.
Git Safety Net fits situations like: the user fears lost work; wants to recover a commit; asks if a branch is actually merged; asks whether a branch.
Run `npx skills add daymade/claude-code-skills --skill git-safety-net -a claude-code`. Or copy the skill folder (git-safety-net in daymade/claude-code-skills) into .claude/skills/git-safety-net in your project. Claude Code loads it when a task matches its description.
Run `npx skills add daymade/claude-code-skills --skill git-safety-net -a codex`. Or copy the skill folder (git-safety-net in daymade/claude-code-skills) into .agents/skills/git-safety-net in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add daymade/claude-code-skills --skill git-safety-net -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/git-safety-net, .gemini/skills/git-safety-net, .github/skills/git-safety-net and .opencode/skills/git-safety-net in your project.
Going by SKILL.md and its folder, Git Safety Net needs a shell and Python for the scripts in its folder and the command-line tools its instructions call (git, gh, uv and bundle). Our summary lists: Python 3; A Bash shell.
SKILL.md contains no URLs. Its commands use git, gh and uv, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Git Safety Net is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 17k tokens (SKILL.md is roughly 66k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 31k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Git Safety Net: Clean Complete Branches (jtenniswood/espcontrol, 1.1k stars), Project Session Manager (Yeachan-Heo/oh-my-claudecode, 40k stars), Conventional Git (samber/cc-skills, 228 stars) and Finishing a Development Branch (obra/superpowers, 296k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
daymade (a GitHub user) maintains it in daymade/claude-code-skills, which has 1,444 GitHub stars. The repository holds 103 skills in this directory. The repository was last updated on October 8, 2026.
Source: daymade/claude-code-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.