The verifier warned that landed work was "unrecoverable" on every single merge. Fixing it surfaced a shell injection in the same file.
git cherry could never work heregit cherry matches patch-ids ONE-TO-ONE. A squash turns N commits into ONE commit with a COMBINED patch. N = 1 → combined patch == the original patch → "-" → guard works N > 1 → combined patch matches NO original → "+" → FALSE ALARM
Proven in a clean synthetic repo on 2026-08-13. The N=1 case working by accident is why this survived from 2026-04-17 in a repo that squash-merges every PR.
| Branch | N | git cherry (shipped) | reverse-apply (fix) |
|---|---|---|---|
| fix/3455-push-refspec | 1 | warns | merged |
| dep3379-local | 2 | warns | merged |
| dep3381-local | 2 | warns | merged |
| fix/elicit-doc-leftovers | 2 | warns | merged |
| feat/3315-worktree-baseref-include | 1 | warns | warns |
A tree-comparison predicate was also tested and rejected: dist/ bundles regenerate on every build, so base legitimately differs from the branch on files whose source change did land.
$ git check-ref-format --branch 'evil;id' # ACCEPTED (only spaces are rejected)
$ git branch 'pwn;touch${IFS}/tmp/marker' # git creates it happily
hook did: execSync(`git rev-parse ${branch}`)
result: evil
uid=501(yonatangross) gid=20(staff) ... <-- `id` EXECUTED
The branch name is read out of git worktree list --porcelain and was interpolated into five shell strings. ${IFS} supplies the space check-ref-format forbids literally, so a payload needing arguments still runs. Trigger: an operator running git worktree remove.
Fix execFileSync — every git call now goes through one git(dir, args[]) helper with an argument array and no shell, so a metacharacter branch is one opaque argv entry.
| Situation | Old message | New message |
|---|---|---|
| branch pushed to origin | "unrecoverable" | "recoverable from the remote" |
| branch local only | "unrecoverable" | "not on origin — push first" |
False urgency on every correct cleanup is what teaches an operator to ignore the warning — which is exactly when a genuine one gets missed.
8 new tests, real throwaway git repos, no git mocking. Proven non-vacuous by swapping in origin/main's pre-fix file: N=2 containment → FAIL (function absent) injection marker → FAIL "expected true to be false" ← payload EXECUTED post-fix → 8/8 pass · 19/19 across both suites