#3365 — an ancestry check in a squash-merge repo

The verifier warned that landed work was "unrecoverable" on every single merge. Fixing it surfaced a shell injection in the same file.

Bug 1 — why git cherry could never work here

git 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.

N=1 squash N=2 squash genuinely unmerged

Measured on this repo's real branches

BranchNgit cherry (shipped)reverse-apply (fix)
fix/3455-push-refspec1warnsmerged
dep3379-local2warnsmerged
dep3381-local2warnsmerged
fix/elicit-doc-leftovers2warnsmerged
feat/3315-worktree-baseref-include1warnswarns

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.

Bug 2 RCE — found while fixing bug 1

$ 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.

Bug 3 — the word "unrecoverable" was simply false

SituationOld messageNew 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.

Tests

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