git-safety-net
Audits, preserves, recovers, and safely retires local Git state: unpushed or wrong-branch commits, dirty or detached worktrees, forgotten duplicate clones of the same repo, untracked work no bundle can back up, orphaned stashes, dangling commits, stale branches, and squash/rebase merge uncertainty. Use when the user fears work was lost; asks to recover a commit or branch; asks whether a worktree, clone, or scratch directory can be deleted; wants everything converged onto one main branch; or needs proof that cleanup will not drop work. Use it even after an audit reported clean — the usual gap is scope: every in-repo command is blind to a second clone elsewhere on disk. Triggers on "did I lose work", "is everything merged", "is anything else lost", "safe to delete this clone", "clean up old branches/stashes", "only keep one main branch", "git reflog", "dangling commits", "分支灾难", "误删分支/commit", "worktree 能删吗", "还有没有丢的东西", "只保留一个主分支". Covers local-Git forensics, not GitHub PR/API operations or routine sync.
1How It Works & Mechanism
The overall architecture consists of an orchestration layer (`SKILL.md`) paired with 6 bundled Bash forensics scripts, all of which are strictly non-destructive and incremental prior to any destructive actions—the recovery process itself must never exacerbate data loss.
until a step is explicitly labeled destructive — recovery must never make the loss worse.
The authoritative check for 'what will I lose?' is executed by `git_loss_audit.sh`: it first runs `fetch` to refresh remote tracking refs, then enumerates the HEAD and status of every linked worktree, local branches/tags, stashes, and dangling commits, exiting with code 1 if any local-only commits or dirty/unreadable worktrees are detected.
# It refreshes remote-tracking refs with `fetch`, then only inspects repository/worktree state. It
To guard against 'the audit reports clean, but the work actually lives in another clone': `git_find_all_checkouts.sh` discovers checkouts at the machine level, identifying other checkouts of the same repository by normalizing remote URLs (treating SSH and HTTPS as equivalent) or by matching shared commit history, never by directory name.
# not `git worktree list`, not `git branch -a`, not `git fsck`, not `git stash list`,
Merge verification is determined by content-level trial merges rather than commit counting: `git merge-tree --write-tree <base> <branch>` merges the branch into the base in memory. If the resulting tree equals the base tree, it is marked as MERGED (content contained); otherwise, it is flagged as NEEDS REVIEW.
# 2. Else do a trial 3-way merge of the branch INTO the base (git merge-tree, in memory, no
Pre-GC preservation: Pins every dangling commit under the hidden ref namespace `refs/dangling-backup/<sha>`—objects pointed to by a ref are immune to garbage collection and will not pollute `git branch` or `git stash list`.
git update-ref "refs/dangling-backup/$sha" "$sha"
Pre-destruction export: Exports stashes as complete binary patches and packages the stash's third parent commit (untracked files captured via `git stash -u/-a`) into a tarball using `git archive "$ref^3"`—because `git stash show -p` ignores these files, relying on patches alone causes silent data loss.
# -> stashN-untracked.tar the often-forgotten THIRD parent:
Pre-deletion compare-and-swap gate: `--verify-current BUNDLE` compares the refs recorded inside the bundle against the repository's current state one by one, exiting with code 1 if any ref has moved or disappeared.
echo "REFSET_CHANGED checked=$CHECKED changed=$MOVED — rebuild and re-verify the bundle before deletion" >&2
Independent clone retirement preparation: The script audits against an extensive rejection checklist (dirty/untracked/ignored bytes, stashes, reflog-only commits, clone-unique unreachable objects, linked worktrees, local submodules, partial/promisor clones, Git LFS/annex object stores, tracked content filters, `include.path`/`hooksPath` indirections, shallow clones), freezing evidence only after all checks pass.
# It refuses to certify a clone that still has working-tree bytes, ignored files,
Strictly bounded write surfaces: The backup directory must be an absolute path, must not already exist (overwrites are rejected), and must reside entirely outside both the target clone and the surviving repository; the script itself never moves, deletes, or modifies refs.
"$CLONE/"*|"$SURVIVOR/"*) die "backup directory must be outside both repositories" ;;
Workflow discipline is embedded in the prompt via 'Outcome contracts + Authorization boundaries': Evidence scope does not equal action scope; authorization is granted per object rather than per repository; preservation and retirement remain strictly phased; and success criteria rely on observable facts in the user's domain (e.g., commit exists on remote, correct branch/worktree states) rather than bundle counts or checksums.
- **Evidence scope is not action scope.** A read-only audit may discover another clone, ref,
The script manifest table explicitly annotates the mutation impact of each script (read-only / touches remote-tracking refs only / appends refs only / writes backup files only) and explicitly disclaims authority for any helper to run checkout, reset, push, stash drop, branch -d, or gc.
the helpers authorizes `checkout`, `reset`, `push`, `stash drop`, `branch -d`, or `gc`.
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| cli | git(含 worktree/fsck/reflog/bundle/merge-tree/update-ref/for-each-ref 等) |
| cli | git ≥ 2.38(merge-tree --write-tree 是内容级判定的前提) |
| cli | uv(仅维护者跑回归测试时用到) |
| cli | sha256sum / shasum(bundle 指纹;缺失即报错) |
| cli | tar(打包 config/hooks/info 元数据) |
| cli | stat(跨 BSD/GNU 的 mtime 与文件模式读取) |
| cli | lsof(macOS 上探测 clone 是否仍被进程占用;仅文档步骤,不在脚本内) |
| network | git fetch(由脚本执行;对远端主机发起出站连接,可能触发宿主 git 凭证助手) |
| network | git fetch --all --prune(合并判定脚本执行,同一出站面) |
| network | git push(仅文本指导:把 backup 分支推到 origin 做三重备份;无脚本执行) |
| package | 无第三方包(Git + 标准 Bash 即可;测试仅用 Python 标准库 unittest/tempfile/subprocess) |
4Recommended Governance & Evidence
- scripts/: Executes local scripts or commands: `git-safety-net/scripts/git_find_all_checkouts.sh` — Read-only: `find` + read-only Git queries (`status`, `rev-list`)
- scripts/ 或 SKILL.md: Declares dynamic installation of external language package dependencies
- 网络端点: Declares external network access: Inbound/Outbound: `git fetch "$REMOTE" --prune` (`git-safety-net/scripts/git_loss_audit.sh`)
- 本地工作目录: File I/O and artifact output: Out-of-repo backups: `git_export_before_drop.sh` defaults to `$HOME/.git-backups/<YYYY-MM-DD>-<repo>/`
5Second-Pass Verification
- [ok] external_deps 逐条反查调用点:git 系列命令 — 在 6 个脚本里逐条确认 git 子命令的真实执行点(fetch/worktree list/fsck --dangling/stash list/bundle create+verify/list-heads/reflog/for-each-ref/update-ref/check-attr/submodule status/cat-file/rev-list)。全部为执行路径上的真实调用,非注释。
- [ok] external_deps:uv 与 lsof 是否真在脚本里 — 两者都不在脚本中执行:`uv run python -m unittest discover -s tests -p 'test_*.py'` 只出现在 SKILL.md § Maintainer verification(维护者本地跑测试);lsof 只出现在 references/merge_verification.md 的退役 READ-DO 序列(macOS 探测进程占用)。已按『文档层工具』而非『运行时依赖』归类。
- [ok] 网络调用面是否存在遗漏(隐藏脚本/混淆内容) — glob 全量(hidden=true, gitignore=false)显示该 skill 目录仅 17 个文件:SKILL.md、3 个 references、6 个 scripts、6 个 tests、evals/evals.json、agents/openai.yaml,无可执行二进制、无压缩/混淆内容。全目录 grep curl/wget/gh/nc/python -c 零命中;唯一 https:// 出现在 find_all_checkouts 的 URL 归一化注释示例里。
- [ok] 反例排查:git_find_all_checkouts.sh 号称不联网,但脚本里有 `git fetch --all --prune` — 该行(第 317 行)位于 `cat <<'FRESHNESS'` heredoc 内,是打印给用户看的建议,不在执行路径上;脚本头部 `# NON-DESTRUCTIVE: runs only \`find\` plus read-only Git queries` 与代码一致(唯一外部命令是 find 与只读 git 查询)。不构成 discrepancy,因此改为标注为『文本指导』。
- [ok] 反查『脚本非破坏性』是否属实(是否有删除/改写仓库对象) — 对 rm -rf / branch -D / reset --hard / stash drop / worktree remove / update-ref -d / gc 全目录扫描:脚本内仅两处相关文本——preserve_danglers 的 `rm -f "$OUT"`(删除自己刚写失败的 patch 文件)与 loss_audit 的提示字符串 `never use 'git worktree remove --force'`。所有真正的破坏性命令(`git update-ref -d`、`git worktree remove`、`--force-with-lease` 等)只出现在 SKILL.md/references 供人执行的步骤中。
- [ok] 『写操作面』核对:备份到底写到哪里、是否会覆盖既有数据 — export 脚本默认写 $HOME/.git-backups/<date>-<repo> 并可 --out 覆盖;preserve_danglers 的 --patch-dir 先绝对化再 mkdir -p;prepare_clone_retirement 强制 `[ ! -e "$OUT" ]`(拒绝已存在目录)、要求绝对路径、并 `case "$OUT/" in "$CLONE/"*|"$SURVIVOR/"*) die ...` 拒绝把备份放进被退役/保留的仓库内。三处均与 SKILL.md『备份放仓库外』一致。
- [ok] 凭证/全局配置读取面反查 — 全目录 grep --global / .gitconfig / credential / HOME 的结果:脚本内只有 export 的 `$HOME/.git-backups/...` 默认输出路径;`git config --global` 只出现在两处 references 中作为给用户的建议命令。prepare_clone_retirement 读取仓库私有配置时显式用 `git config --file <config> --no-includes`(不吞并用户全局配置)。无 token/env 读取。
- [ok] 实现原理是否夸大:『内容级判定是 sound 而非启发式』 — 与脚本逐行比对成立:脚本先 `git merge-base --is-ancestor` 判 ancestor,再 `git merge-tree --write-tree <base> <branch>` 比较结果树与 base 树;MT_RC>=128(git<2.38)时明确退回 NEEDS REVIEW 而非猜 MERGED。声明与实现一致,未夸大。
6Verdict & Scenarios
b2b944f8a078459b…