[object Object]

← 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

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 →