← back to Nas Setup
TK-11648: identity guard so backup never bundles the WRONG repo (false-green)
960300842835863797236ad87c3f604633403620 · 2026-09-14 00:00:01 -0700 · Steve Abrams
backup-repos-to-henry.sh guarded only that .git EXISTS and that the bundle
passes 'git bundle verify' — which proves a VALID history, not THIS repo's.
A present-but-hollow child .git makes 'git bundle create --all' walk UP to
the ~/Projects meta-repo and bundle THAT under the child's name: a valid
bundle of the wrong repo, so the ONLY off-machine copy (no-remotes rule) is
silently worthless while the run reads PASS. Confirmed: animals.bundle was a
1.6G copy of the meta-repo (HEAD resolved as a meta-repo commit).
FIX: assert 'git -C $repodir rev-parse --show-toplevel' == $repodir before
bundling; a mismatch is a FAIL, never a silent wrong bundle. Ships a NEGATIVE
test (test-backup-identity-guard.sh, per TK-11431 amdt 3): injects a hollow
child .git, proves the fault reproduces, the guard FAILs it, and a valid repo
still PASSes. Test PASS.
AUDIT (read-only, 1207 bundles): 3 mislabeled — animals (source .git since
REPAIRED, self-heals on next run), govarbitrage.gutted-1789157670 + japan-enrich
(hollow .git, need repair; guard now FAILs them instead of mislabeling). Repair
drafted to pending-approval (judgment work; gitify rule = don't re-init a .git).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
Files touched
M scripts/backup-repos-to-henry.shA scripts/test-backup-identity-guard.sh
Diff
commit 960300842835863797236ad87c3f604633403620
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Sep 14 00:00:01 2026 -0700
TK-11648: identity guard so backup never bundles the WRONG repo (false-green)
backup-repos-to-henry.sh guarded only that .git EXISTS and that the bundle
passes 'git bundle verify' — which proves a VALID history, not THIS repo's.
A present-but-hollow child .git makes 'git bundle create --all' walk UP to
the ~/Projects meta-repo and bundle THAT under the child's name: a valid
bundle of the wrong repo, so the ONLY off-machine copy (no-remotes rule) is
silently worthless while the run reads PASS. Confirmed: animals.bundle was a
1.6G copy of the meta-repo (HEAD resolved as a meta-repo commit).
FIX: assert 'git -C $repodir rev-parse --show-toplevel' == $repodir before
bundling; a mismatch is a FAIL, never a silent wrong bundle. Ships a NEGATIVE
test (test-backup-identity-guard.sh, per TK-11431 amdt 3): injects a hollow
child .git, proves the fault reproduces, the guard FAILs it, and a valid repo
still PASSes. Test PASS.
AUDIT (read-only, 1207 bundles): 3 mislabeled — animals (source .git since
REPAIRED, self-heals on next run), govarbitrage.gutted-1789157670 + japan-enrich
(hollow .git, need repair; guard now FAILs them instead of mislabeling). Repair
drafted to pending-approval (judgment work; gitify rule = don't re-init a .git).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVgEj4WxSqB62ieGpgrixR
---
scripts/backup-repos-to-henry.sh | 16 +++++++++++
scripts/test-backup-identity-guard.sh | 51 +++++++++++++++++++++++++++++++++++
2 files changed, 67 insertions(+)
diff --git a/scripts/backup-repos-to-henry.sh b/scripts/backup-repos-to-henry.sh
index ac74665..41c2c0f 100755
--- a/scripts/backup-repos-to-henry.sh
+++ b/scripts/backup-repos-to-henry.sh
@@ -38,6 +38,22 @@ for gd in "$SRC"/*/.git "$HOME/.claude/.git" "$HOME"/.claude/skills/*/.git; do
*) repo="$(basename "$repodir")" ;;
esac
tmp="$DEST/.$repo.bundle.tmp"; final="$DEST/$repo.bundle"
+ # TK-11648 IDENTITY GUARD: `git bundle verify` (below) proves the bundle is a VALID
+ # history — never that it is THIS repo's. If $repodir/.git is present-but-invalid (a
+ # hollow skeleton, no HEAD/objects), git walks UP to the nearest valid repo (the
+ # ~/Projects meta-repo) and `bundle create --all` bundles THAT under the child's name:
+ # a valid bundle of the WRONG repo, so the only off-machine copy is silently worthless
+ # while the run still reads PASS (confirmed: animals.bundle was a 1.6G copy of the
+ # meta-repo). Assert git resolves to THIS dir before bundling; a mismatch is a FAIL, not
+ # a silent wrong bundle. This measures what the bundle's NAME claims — exactly what
+ # `git bundle verify` structurally cannot.
+ want="$(cd "$repodir" 2>/dev/null && pwd -P)"
+ top="$(git -C "$repodir" rev-parse --show-toplevel 2>/dev/null)"
+ if [ -z "$want" ] || [ -z "$top" ] || [ "$top" != "$want" ]; then
+ rm -f "$tmp" 2>/dev/null; fail=$((fail+1))
+ echo " FAIL: $repo has an invalid .git (git resolves to ${top:-<none>}, not $want) — NOT bundling"
+ continue
+ fi
if git -C "$(dirname "$gd")" bundle create "$tmp" --all >/dev/null 2>&1 && git -C "$(dirname "$gd")" bundle verify "$tmp" >/dev/null 2>&1; then
mv -f "$tmp" "$final"; ok=$((ok+1))
else
diff --git a/scripts/test-backup-identity-guard.sh b/scripts/test-backup-identity-guard.sh
new file mode 100755
index 0000000..cce4284
--- /dev/null
+++ b/scripts/test-backup-identity-guard.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# TK-11648 NEGATIVE TEST for backup-repos-to-henry.sh's identity guard.
+# CLAUDE.md TK-11431 amendment 3: a positive-only test on a detector proves nothing —
+# break it on purpose, watch it go red, put it back.
+#
+# The fault: a child dir with a present-but-INVALID .git makes `git bundle create --all`
+# walk UP to the nearest valid parent repo and bundle THAT under the child's name — a
+# VALID bundle (so `git bundle verify` passes) of the WRONG repo. This test injects that
+# exact shape and asserts the guard FAILs it, and that a valid repo PASSes.
+set -uo pipefail
+T="$(mktemp -d /tmp/tk11648-XXXX)"
+trap 'rm -rf "$T"' EXIT
+fails=0
+
+# --- fixture -----------------------------------------------------------------
+# meta = a valid parent repo (mimics ~/Projects); child = hollow .git under it.
+git init -q "$T/meta"; ( cd "$T/meta" && git -c user.email=t@t -c user.name=t commit -q --allow-empty -m meta )
+mkdir -p "$T/meta/child/.git/refs" "$T/meta/child/.git/objects" # hollow: no HEAD/config
+# good = a valid standalone repo.
+git init -q "$T/good"; ( cd "$T/good" && git -c user.email=t@t -c user.name=t commit -q --allow-empty -m good )
+
+# the guard snippet, verbatim from backup-repos-to-henry.sh
+guard() { # $1 = repodir -> prints PASS|FAIL
+ local repodir="$1" want top
+ want="$(cd "$repodir" 2>/dev/null && pwd -P)"
+ top="$(git -C "$repodir" rev-parse --show-toplevel 2>/dev/null)"
+ if [ -z "$want" ] || [ -z "$top" ] || [ "$top" != "$want" ]; then echo "FAIL"; else echo "PASS"; fi
+}
+
+# --- prove the FAULT exists (what the guard defends against) ------------------
+# Without the guard, `bundle create --all` on the hollow child succeeds by walking up:
+if git -C "$T/meta/child" bundle create "$T/child.bundle" --all >/dev/null 2>&1 \
+ && git bundle verify "$T/child.bundle" >/dev/null 2>&1; then
+ bh="$(git bundle list-heads "$T/child.bundle" 2>/dev/null | head -1 | awk '{print $1}')"
+ if git -C "$T/meta" cat-file -t "$bh" 2>/dev/null | grep -q commit; then
+ echo "ok : FAULT reproduced — hollow child bundled the PARENT repo (verify passed on the wrong repo)"
+ else
+ echo "FAIL: expected the hollow child to bundle the parent, but it did not"; fails=$((fails+1))
+ fi
+else
+ echo "ok : (this git refused to bundle the hollow child outright — also acceptable)"
+fi
+
+# --- the guard's verdicts -----------------------------------------------------
+r_child="$(guard "$T/meta/child")"
+r_good="$(guard "$T/good")"
+[ "$r_child" = "FAIL" ] && echo "ok : guard FAILs the hollow child (walk-up caught)" || { echo "FAIL: guard did NOT catch the hollow child (got $r_child)"; fails=$((fails+1)); }
+[ "$r_good" = "PASS" ] && echo "ok : guard PASSes the valid standalone repo" || { echo "FAIL: guard wrongly rejected a valid repo (got $r_good)"; fails=$((fails+1)); }
+
+echo "-------------------------------------------"
+[ "$fails" -eq 0 ] && { echo "TK-11648 guard test: PASS"; exit 0; } || { echo "TK-11648 guard test: FAIL ($fails)"; exit 1; }
← e6b27ef daemon-health: watch the repo-bundle belt, not just the two
·
back to Nas Setup
·
TK-11648: negative test drives the REAL backup script via a 808d749 →