Use when verifying that an upstream commit (e.g., a colleague's work, a feature-branch commit) is preserved in a target branch after a propagation PR has merged — and the verification script uses `git merge-base --is-ancestor <upstream-sha> <target-branch>`. If the merge mode was **squash**, that ancestor check ALWAYS returns non-zero even when content is preserved verbatim, because squash collapses every commit into a single new SHA on the target. Apply this skill when: (1) preservation audi...
Scanned 9/6/2026
Install to Claude Code
npx -y skills add wan-huiyan/agent-traffic-control --skill squash-merge-content-preservation-vs-ancestor-check --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Squash Merge Content Preservation Vs Ancestor Check?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/wan-huiyan-squash-merge-content-preservation-vs-ancestor-chec)More formats (shields.io, HTML) on the badges page.
---
name: squash-merge-content-preservation-vs-ancestor-check
description: |
Use when verifying that an upstream commit (e.g., a colleague's work, a
feature-branch commit) is preserved in a target branch after a propagation
PR has merged — and the verification script uses
`git merge-base --is-ancestor <upstream-sha> <target-branch>`. If the merge
mode was **squash**, that ancestor check ALWAYS returns non-zero even when
content is preserved verbatim, because squash collapses every commit into a
single new SHA on the target. Apply this skill when: (1) preservation audit
for a recently-merged propagation PR returns "MISSING" or non-zero exit on
`git merge-base --is-ancestor`, (2) handoff prompt mixes `is-ancestor` AND
content checks without specifying merge mode, (3) you need to gate a merge
on "did colleague's signature work survive". The correct probe depends on
merge mode — fast-forward / no-ff preserves lineage; squash does not.
version: 1.0.0
date: 2026-05-27
author: Huiyan / Claude Opus 4.7
disable-model-invocation: true
---
# Squash Merge Content Preservation vs Ancestor Check
## Problem
Your verification script says:
```bash
git merge-base --is-ancestor 0b6303e76 origin/main && echo "OK" || echo "MISSING"
# Prints: MISSING
```
… but `wc -l <signature-file>` returns the expected line counts and `grep -c
<signature-string>` returns ≥ the expected ref count. The exit code from
`is-ancestor` is non-zero **even though every byte of the upstream work is
preserved on the target branch**. The "MISSING" verdict is wrong.
Root cause: a squash merge collapses every commit on the source branch into a
**single new commit** on the target. The original SHAs (including the upstream
commit you're checking) never appear in the target's history. `is-ancestor`
returns "false" because the SHA truly isn't an ancestor of the merge commit —
but the SHA's *content* is preserved verbatim inside the squash commit.
This trap fires whenever:
- A handoff prompt or skill mixes ancestor-based checks with content-based
checks for the same preservation claim
- The handoff was written before the merge mode was decided
- The verification template assumed fast-forward / no-ff (which preserves
lineage) but the actual merge used squash
## Context / Trigger Conditions
You're in the right place if **all** of these are true:
- A propagation-style PR (`branch-A → branch-B`, often 20+ commits ahead) has
recently merged
- The verification step instructs you to check `git merge-base --is-ancestor
<upstream-sha> <target-branch>` AND content-level probes (line counts, grep
ref counts, file existence)
- The ancestor probe returns non-zero / "MISSING" / exit 1
- The content probes all PASS (line counts match, ref counts ≥ expected,
signature files exist)
- The PR's merge method was **squash** (check via `gh pr view <N> --json
mergeCommit,mergedBy` — the merge commit's parent commits are NOT the source
branch's tip)
The trap also applies in reverse direction: any check that asks "is this old
commit in our new history?" will fail for squash workflows.
## Solution
### Step 1 — Confirm the merge was a squash
```bash
gh pr view <PR-N> -R <owner>/<repo> --json mergeCommit
# Inspect: the merge commit has ONE parent (the previous target HEAD),
# NOT two parents (target HEAD + source branch tip). One-parent = squash;
# two-parents = no-ff merge.
```
Or in git:
```bash
git log -1 --format='%P' <merge-sha>
# One SHA printed = squash or fast-forward
# Two SHAs printed = no-ff or merge commit
```
If two-parent (no-ff/merge), `is-ancestor` SHOULD work — diagnose further
(maybe the SHA is wrong, or the target branch is stale).
### Step 2 — Replace ancestor checks with content-level checks
For the same preservation claim, swap to content probes:
```bash
# File existence + line count
for f in path/to/sig1.py path/to/sig2.py; do
echo -n "$f: "
git show origin/main:$f 2>/dev/null | wc -l
done
# Expect known line counts from the upstream work
# Grep ref count
git show origin/main:path/to/consumer.py | grep -c "<upstream-symbol>"
# Expect >= N from the upstream registration
# Byte-identical SHA on a known block (strongest proof — see
# `byte-identical-sha-gate-as-refactor-safety-net`)
for branch in origin/main <source-branch-pre-merge>; do
git show $branch:path/to/registry.py | sed -n '<start>,<end>p' | sha256sum
done
# Two matching SHAs = block-level byte preservation proven
```
### Step 3 — Update the verification template
If a handoff prompt, runbook, or skill recommended the ancestor check
unconditionally, fork the recommendation on merge mode:
```markdown
**Verification depends on merge mode:**
- Fast-forward or no-ff merge → `git merge-base --is-ancestor <sha> <target>`
- **Squash merge** → ancestor check ALWAYS fails by design. Use content
probes: file existence, line counts, grep ref counts, byte-identical SHA
on signature blocks.
- Rebase merge → ancestor fails on original SHA but rebased equivalent may
exist; use `git log --cherry-mark <target>...<source>` for definitive
answer.
```
### Step 4 — When squash is the team default, default to content checks
If your team uses squash merge as the policy default (GitHub's "Squash and
merge" button), pre-write preservation audits using content checks only.
Don't write `is-ancestor` into the audit at all — it'll be wrong every time.
## Verification
A correct audit after squash propagation produces:
```
=== Katie preservation check ===
(a) ancestor check: SKIP (squash merge — see L-256)
(b) signature file line counts on origin/main:
google_trends_covariates.py: 703 ← matches expected
weather_covariates.py: 271 ← matches expected
validation_tests.py: 303 ← matches expected
(c) brand-share daily ref count on origin/main: 9 ← ≥ 5 expected
=== Verdict: content preserved ===
```
If `is-ancestor` is still in the script, mark it as expected-to-fail with a
comment, or delete it.
## Example
Session 72 (2026-05-27) propagated `chore/promote-release-uk-to-main → main`
via squash-merge (PR #146 → commit `abe1cad7`). The S72 prompt included this
verification block:
```bash
git merge-base --is-ancestor 0b6303e76 HEAD && echo "OK" || echo "MISSING"
wc -l webapp/google_trends_covariates.py # expect 703
wc -l webapp/weather_covariates.py # expect 271
wc -l webapp/validation_tests.py # expect ~303
grep -c "google_trends_brand" webapp/research_utils.py # expect >= 5
```
Post-merge result:
- Ancestor check: **MISSING** (0b6303e76 not in origin/main's history)
- Line counts: 703 / 271 / 303 ✓
- grep ref count: 9 ≥ 5 ✓
Initial reaction was confusion — was Katie's work clobbered? No. The squash
merge collapsed all 35 source-branch commits into the single new SHA
`abe1cad7`. Content preserved; lineage rewritten. The ancestor probe was the
wrong tool. Recovery: trust the content checks, update the verification
template (this skill + L-256 in lessons.md).
## Notes
- **Sister skill**: `byte-identical-sha-gate-as-refactor-safety-net` — when
you want a stronger proof than line counts (block-level SHA equality
between source and target).
- **Related**: `pre-merge-client-variant-regression-audit` Step 5 already
uses byte-identical SHA for the right reason (regression detection on
enrichment registration blocks). Apply the same content-only discipline
to post-merge preservation audits.
- **Related**: `gh-squash-merge-closes-only-one-issue` — another
squash-merge surprise (closes only one `Closes #N` issue per PR body).
- The bug isn't `is-ancestor` — it's mixing it with content checks under a
single preservation claim without specifying merge mode. Either probe is
fine when scoped to its merge-mode assumption.
- Some teams use `gh api PUT` with `merge_method=squash` to merge from
worktree contexts (per `gh-pr-merge-worktree-checkout-trap`); when that's
your default merge call, your verification templates MUST assume squash.
## References
- Git docs: [`git merge-base`](https://git-scm.com/docs/git-merge-base) —
documents that `--is-ancestor` returns 0/1 based on commit-DAG reachability,
not content equivalence.
- GitHub docs: [About merge methods](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-merge-methods-on-github)
— squash merge collapses commits into one; "the commits on the head branch
are not preserved in the history."
- Project memory: `~/.claude/projects/-Users-<user>-Documents-the-causal-impact-repo/memory/lessons.md` L-256.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!