Skip to main content

git-safety-net

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.

来源信息

仓库
daymade/claude-code-skills
最近来源活动
2026年10月23日 16:02
检测到的 SKILL.md 语言
英语
星标
1,496
Forks
229

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
25 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
git-safety-net
description
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.
# 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. ## Outcome contract — keep the safety net subordinate to the user's job Before Mode B/E or any command that writes a ref or backup, state the following in the conversation (do not create another file): - **Outcome:** the user-visible end state, in the user's words. - **Current phase:** what is authorized now. "Later" work is not authorized in this phase. - **Authorized targets:** named objects this phase may inspect, plus the subset it may change. - **Stop condition:** observable facts that end the task. Then enforce these boundaries: - **Evidence scope is not action scope.** A read-only audit may discover another clone, ref, repository, or dangling object. That discovery may widen the report; it does not authorize preserving, uploading, merging, deleting, or otherwise changing the newly found object. - **Authorization is object-specific, not repository-wide by implication.** "Take over", "continue", or "finish the cleanup" applies only to the checkout/ref/PR the user identified. A collaborator-owned worktree, branch, or PR discovered later stays report-only until the user names it as a change target. If the user says to leave it alone, record that exclusion and do not inspect its working-tree contents, back it up, merge it, unlock/remove it, or mutate its refs. - **Preserve the smallest set threatened by the next authorized destructive action.** If the current phase is only commit/push/verify and no deletion, reset, gc, history rewrite, or worktree removal is authorized, do not create an all-refs bundle or pin every dangler. - **Classify an artifact before choosing its transport.** Durable project source follows the repository's normal Git/LFS policy. A temporary recovery artifact (bundle, working-tree diff, snapshot, transport chunk) belongs in a repository-external backup directory. Do not stage, commit, push, or route it through Git LFS merely to make the backup remote; Git LFS is for durable versioned project binaries, not a fallback transport for temporary recovery material. - **Keep preservation and retirement as separate phases.** A verified recovery artifact proves recoverability; it does not prove the business code landed. Keep it through the destructive action and the content-level postconditions. Retire an exact task-owned temporary artifact only when every payload is landed or proven superseded and the artifact itself has explicit cleanup authorization. Keep backup-only, unresolved, and mixed artifacts. - **Treat a new storage or execution surface as a scope change.** A second repository, new remote, cloud upload, Git LFS, or full-history export requires re-planning and explicit authority when the stated outcome actually depends on it. Do not solve a transport problem the user did not ask to create. - **Prove completion in the user's world.** A remote containing the intended commit, preserved WIP, and the requested branch/worktree state are outcomes. Bundle counts, checksums, upload receipts, and audit breadth are supporting evidence, never substitutes for that outcome. Stop when the contract is satisfied; record unrelated findings separately without acting on them. Before withholding otherwise authorized commits or pushes because a handover says "do not upload", verify the original restriction's object, action and destination. Use [the convergence authority check](references/merge_verification.md#1-freeze-the-outcome-and-the-first-ref-snapshot); an assistant-authored restriction is a lookup lead, not a new user decision. ## Entry router — pick the mode from what the user is worried about | The user says / needs… | Go to | |---|---| | Local snapshots are blocked by an `index.lock` or commit-hook refusal | [Blocked local snapshots](references/recovery_playbook.md#blocked-local-snapshots) — preserve the lock and staged state; diagnose both layers before an authorized recovery | | "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. ## Load-bearing rules (internalize these; the modes apply them) 1. **Get the EVIDENCE SCOPE right before you trust any verdict, without silently expanding the work scope: 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 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. 2. **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, tags, and every other commit-holding local ref against every remote, then inspects each worktree for tracked/untracked changes plus stashes, reflog-only and dangling commits. Reflog-only work includes every linked-worktree HEAD log; it stays advisory but prevents a zero-risk verdict. Failed content scans or Git diagnostics make the audit incomplete (exit 1); they never establish a clean result. Non-commit refs are listed for manual object-preservation review and suppress the zero-risk verdict without raising the commit-risk exit code. The shorter `git log HEAD --branches --tags --not --remotes` misses a detached HEAD in a different worktree, commits pinned only by non-branch/tag refs, 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, reflogs, 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. 3. **`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. 4. **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 add`ed and never `stash -u`ed 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. 5. **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](references/merge_verification.md)**. 6. **For a high-stakes exhaustive "is everything merged?" call that will authorize deletion, verify adversarially.** One independent reviewer is the default. Use multiple reviewers only when distinct repositories or evidence axes cannot be covered by one pass and the user has authorized that fan-out. Make one pass try to falsify the declared evidence scope (rule 1), but keep any newly found target report-only under the Outcome contract. ## Mode A — Recover lost work 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](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: ```bash 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, fall through by symptom rather than reaching for `fsck` first: a dropped stash has its own recovery route (**[references/recovery_playbook.md](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). ## Mode B — Audit what's at risk, then preserve it **Step 0 — establish the evidence scope (rule 1).** What "at risk" covers, and the checkout kinds that hide it, are **[references/recovery_playbook.md](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: ```bash 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. 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. ### Maintainer verification Run the isolated regression suite after changing checkout discovery: ```bash 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/reflog/stash/dangler the script enumerates is inside the declared evidence scope: ```bash scripts/git_loss_audit.sh # defaults to remote "origin"; pass a remote name to override ``` When any surface listed above is excluded, skip that script and collect only checkout/ref-scoped evidence: ```bash 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 failed ``` Both 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**, **reflog-only commits**, and **dangling commits**, with non-commit refs listed for manual review. Exit is 1 when commits exist on no remote, a worktree is dirty/uninspectable, or a content scan is incomplete. Stashes, reflog-only/dangling commits and non-commit refs remain advisory but prevent the zero-risk verdict. Exit 0 is therefore not permission to delete reported work: 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, reflogs, 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: For reflog-only work, use the targeted pin in the linked preservation procedure; this whole-set helper does not enumerate reflog-only commits. ```bash scripts/git_preserve_danglers.sh --patch-dir ~/git-danglers # pin + export patches ``` Why pinning survives `gc`, and the targeted single-ref form when the whole set is not in scope: **[references/recovery_playbook.md](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](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: ```bash 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) — the mechanism is **[references/merge_verification.md](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): ```bash 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. 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](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](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
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看