From 583634162ea8c092307ab5aa121ec13055657057 Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 19:47:52 -0400 Subject: [PATCH 1/5] repo: pin the content surface to the object store A working-tree scan skipped what `grep -I` calls binary and followed symlinks to their targets, so a tracked blob carrying the name could land while the wall reported clean. The content surface is now every tracked blob, read from the object store, bytewise; a symlink is scanned as the blob it is, never followed; a blob that is not UTF-8 reports `[binary blob]` in place of the line. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T234752Z-apply-push-4f650a Patch-SHA256: 82bfe322636865f3130a554e957100917ee1f99402bea2137f9635d8cc838575 --- .github/actions/term-wall/CONTRACT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/term-wall/CONTRACT.md b/.github/actions/term-wall/CONTRACT.md index a81af96..e54e6f1 100644 --- a/.github/actions/term-wall/CONTRACT.md +++ b/.github/actions/term-wall/CONTRACT.md @@ -95,3 +95,7 @@ no dependence on the caller's cwd, environment, or git identity (configure user.name/user.email in each temp repo). Every test asserts the exit code and the output shape. Push and pull-request events are simulated with an event JSON file and the `GITHUB_*` variables. + +## Content surface, pinned + +The content surface is every blob the scanned commit's tree tracks, read from the object store, never from the working tree. Every blob is scanned bytewise; nothing tracked is unscannable. A symlink entry is scanned as the blob it is, its target path text, and is never followed. A blob that is not valid UTF-8 reports each hit with the location ` line `, `n` counting newline-separated segments from 1, and the third field `[binary blob]` in place of the line. A blob the wall cannot read is a refusal of class `git work tree`: `git work tree: expected a readable blob at ; found ; needed the object`. From 891440e01ed31eec1a6ccf6199476dcef3117ad4 Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 19:50:12 -0400 Subject: [PATCH 2/5] repo: tests for the content surface, from the contract Five subprocess tests, each in its own temporary repository: a blob that is not UTF-8 carrying the planted fault, a symlink whose target text carries it, a clean symlink to a clean file, a clean commit with a dirtied working tree, and a committed fault deleted from the working tree. Written against the pinned contract, before the implementation; red until it moves. Source: original Co-Authored-By: Codex Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T235011Z-apply-push-77cd09 Patch-SHA256: 7b9e0252dd5761078e3b1a1f88134326ef7d53ec10efdb4b55d5b9538c9d5e06 --- .../actions/term-wall/tests/test_term_wall.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.github/actions/term-wall/tests/test_term_wall.py b/.github/actions/term-wall/tests/test_term_wall.py index b3448b7..1976e89 100644 --- a/.github/actions/term-wall/tests/test_term_wall.py +++ b/.github/actions/term-wall/tests/test_term_wall.py @@ -124,6 +124,62 @@ def test_binary_tracked_content_is_skipped(self): self.commit("add binary fixture") self.assert_clean(self.run_wall()) + def test_non_utf8_committed_blob_hit_uses_binary_placeholder(self): + (self.repo / "fixture.bin").write_bytes( + b"ordinary first line\ncontains " + PLANT.encode("ascii") + b" \xff\n" + ) + self.commit("add non-UTF-8 fixture") + + result = self.run_wall() + + self.assertEqual(result.returncode, 1, result) + self.assertEqual(result.stderr, "") + self.assertEqual( + result.stdout, + "content: fixture.bin line 2: [binary blob]\n", + ) + + def test_dangling_symlink_target_text_is_scanned_as_content(self): + os.symlink(f"absent-{PLANT}-target", self.repo / "dangling-link") + self.commit("add dangling symlink") + + result = self.run_wall() + + self.assertEqual(result.returncode, 1, result) + self.assertEqual(result.stderr, "") + self.assertEqual( + result.stdout, + "content: dangling-link line 1: absent-[forbidden name]-target\n", + ) + + def test_clean_symlink_to_committed_clean_file_is_clean(self): + os.symlink("clean.txt", self.repo / "clean-link") + self.commit("add clean symlink") + + self.assert_clean(self.run_wall()) + + def test_uncommitted_working_tree_content_is_not_scanned(self): + self.write("tracked.txt", "committed clean content\n") + self.commit("add clean tracked file") + self.write("tracked.txt", f"working tree contains {PLANT}\n") + + self.assert_clean(self.run_wall()) + + def test_committed_blob_deleted_only_from_working_tree_is_scanned(self): + self.write("deleted.txt", f"committed content contains {PLANT}\n") + self.commit("add planted tracked file") + (self.repo / "deleted.txt").unlink() + + result = self.run_wall() + + self.assertEqual(result.returncode, 1, result) + self.assertEqual(result.stderr, "") + self.assertEqual( + result.stdout, + "content: deleted.txt line 1: committed content contains " + "[forbidden name]\n", + ) + def test_tracked_path_hit_is_masked(self): self.write(f"notes-{PLANT}.txt", "ordinary text\n") self.commit("add fixture") From 5c1a01ecda312782483e739a28a27b52ee93b0b5 Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 19:57:52 -0400 Subject: [PATCH 3/5] repo: scan the content surface from the object store Enumerate the scanned commit's tree with `git ls-tree -r -z`, read each blob with `git cat-file`, scan bytewise; a symlink is scanned as the blob it is and never followed; a blob that is not UTF-8 reports `[binary blob]` in place of the line; a blob that cannot be read is a `git work tree` refusal. Implementation moves to the pinned contract; the tests stand. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T235752Z-apply-push-437ac9 Patch-SHA256: 47bcffbb08b404c5732c0e9ef2ed74a2344887559f096ab5c6dfe6f74048ccd2 --- .github/actions/term-wall/term-wall.sh | 49 +++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/actions/term-wall/term-wall.sh b/.github/actions/term-wall/term-wall.sh index f7b123f..64b8bbe 100755 --- a/.github/actions/term-wall/term-wall.sh +++ b/.github/actions/term-wall/term-wall.sh @@ -10,7 +10,12 @@ # may not. # # Surfaces, in order: -# 1. tracked file content (case-insensitive, binaries skipped) +# 1. tracked file content — every blob the scanned commit's tree +# tracks, read from the object store, never from the working tree, +# and scanned bytewise (case-insensitive): nothing tracked is +# unscannable. A symlink entry is scanned as the blob it is — its +# target path text — and never followed. A blob that is not valid +# UTF-8 reports each hit with "[binary blob]" in place of the line. # 2. tracked file paths # 3. the commit messages of the change — pull_request: base..head; # push: before..head, or only the head commit when before is all @@ -77,13 +82,41 @@ scan_name() { # scan_name — for surfaces whose location emit "$surface" "$masked" "$masked" } -# 1. tracked content -content=$(git ls-files -z 2>/dev/null | xargs -0 -r grep -I -H -i -n -E -- "$pat" 2>/dev/null | mask || true) -if [[ -n $content ]]; then - while IFS= read -r line; do - file=${line%%:*}; rest=${line#*:} - emit "content" "$file line ${rest%%:*}" "${rest#*:}" - done <<< "$content" +# 1. tracked content — every blob of the scanned commit's tree, from the +# object store, bytewise (grep -a, never -I). A symlink entry (mode +# 120000) is a blob holding its target path and is never followed; a +# gitlink (type commit) is not a blob and has no content here. A blob +# the wall cannot read is a refusal — could-not-look is never a pass. +if git rev-parse -q --verify 'HEAD^{commit}' >/dev/null 2>&1; then + git ls-tree -r HEAD >/dev/null 2>&1 \ + || refuse 'git work tree: expected a readable tree at HEAD; found git ls-tree cannot read it; needed the object' + while IFS= read -r -d '' entry; do + meta=${entry%%$'\t'*} + path=${entry#*$'\t'} + read -r _mode type oid <<< "$meta" + [[ $type == blob ]] || continue + if ! err=$(git cat-file -e "$oid" 2>&1); then + err=$(printf '%s' "${err:-git cat-file cannot read $oid}" | tr '\n' ' ') + refuse "$(printf 'git work tree: expected a readable blob at %s; found %s; needed the object' "$path" "$err" | mask)" + fi + if git cat-file blob "$oid" 2>/dev/null | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1; then + # NUL is valid UTF-8 (U+0000) but a shell variable cannot hold + # it — drop it before masking, never after. + found=$(git cat-file blob "$oid" 2>/dev/null | grep -a -i -n -E -- "$pat" | tr -d '\000' | mask || true) + [[ -n $found ]] || continue + while IFS= read -r line; do + emit "content" "$path line ${line%%:*}" "${line#*:}" + done <<< "$found" + else + # Not valid UTF-8: never echo its bytes — only line numbers + # (newline-separated segments, from 1) and "[binary blob]". + found=$(git cat-file blob "$oid" 2>/dev/null | grep -a -i -n -E -- "$pat" | cut -d: -f1 || true) + [[ -n $found ]] || continue + while IFS= read -r n; do + emit "content" "$path line $n" "[binary blob]" + done <<< "$found" + fi + done < <(git ls-tree -r -z HEAD 2>/dev/null) fi # 2. tracked paths From 2fc23a29bd186462ddf8e8b1ef2c59529e229896 Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 19:59:28 -0400 Subject: [PATCH 4/5] repo: retire the test that asserted a skipped binary blob The pinned content surface makes every tracked blob scannable, so the pre-existing test asserting that a binary blob passed as clean contradicts the contract. The binary-blob test written against the amendment covers the case. Source: original Co-Authored-By: Codex Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260901T235927Z-apply-push-91f0e5 Patch-SHA256: 5f57e606b263e185759beacf0f8ec59baa69a00b7b4c23c80e85b3c8e6aa3a9f --- .github/actions/term-wall/tests/test_term_wall.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/actions/term-wall/tests/test_term_wall.py b/.github/actions/term-wall/tests/test_term_wall.py index 1976e89..bdd2e43 100644 --- a/.github/actions/term-wall/tests/test_term_wall.py +++ b/.github/actions/term-wall/tests/test_term_wall.py @@ -119,11 +119,6 @@ def test_tracked_content_hit_is_masked(self): self.commit("add fixture") self.assert_hit(self.run_wall(), "content") - def test_binary_tracked_content_is_skipped(self): - (self.repo / "fixture.bin").write_bytes(b"\x00" + PLANT.encode("ascii") + b"\xff") - self.commit("add binary fixture") - self.assert_clean(self.run_wall()) - def test_non_utf8_committed_blob_hit_uses_binary_placeholder(self): (self.repo / "fixture.bin").write_bytes( b"ordinary first line\ncontains " + PLANT.encode("ascii") + b" \xff\n" From a1e76d027223d943a0c8152971a965926d17c016 Mon Sep 17 00:00:00 2001 From: Apply Push Bridge Date: Tue, 1 Sep 2026 20:00:46 -0400 Subject: [PATCH 5/5] repo: commit the planted fault in the self-test The content surface is the scanned commit's tree, so a planted file that is only staged is not on it. The self-test now commits the plant before expecting the wall to fire. Source: original Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012Jj94rkp3tfHAxUkTCthgY Apply-Push-Job: 20260902T000046Z-apply-push-aa7d57 Patch-SHA256: fa75a5987b8760303465c1ab5e6ec4a2aa9e52100f37a9e9a34a7537baa00fd6 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e66a322..1f688ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: t=$(mktemp -d); cd "$t"; git init -q; git config user.email ci@ci.invalid; git config user.name ci printf 'clean\n' > c.txt; git add c.txt; git commit -qm clean env -u GITHUB_EVENT_PATH bash "$wall" - printf 'x %s y\n' "$TERM_WALL_PLANT" > p.txt; git add p.txt + printf 'x %s y\n' "$TERM_WALL_PLANT" > p.txt; git add p.txt; git commit -qm plant rc=0; out=$(env -u GITHUB_EVENT_PATH bash "$wall") || rc=$? [ "$rc" -eq 1 ] || { echo "::error::the wall did not exit 1 on a planted fault (exit $rc)"; exit 1; } case $out in *"$TERM_WALL_PLANT"*) echo "::error::the wall printed the raw plant"; exit 1;; esac