Skip to main content

The Squash Merge Incident

Incident Date: April 6-7, 2026

A single squash merge silently reverted 25+ previously merged pull requests across 67 files, destroying security fixes, accessibility features, statistical analysis code, and research data. A second squash merge downgraded GitHub Actions versions across 30 workflow files. Full restoration required 6 commits across 2 PRs touching 50+ files.

What Happened

During a batch merge session managed by Claude Code, PR #927 (a 5-file feature adding an effect size chart) was squash-merged into main. The branch had been forked from main approximately 27 hours earlier. During that window, 35 other PRs had been merged to main.

The squash merge silently reverted every change to every file the branch touched - not just the 5 files that were part of the feature, but 62 additional files that had been modified between the branch's fork point and the merge time. GitHub's diff showed 67 changed files, but this red flag was missed.

A second squash merge (PR #803) was identified during the investigation. It had the same root cause: a stale Dependabot-style branch squash-merged without rebasing, reverting action version upgrades across 30 workflow files.

Root Cause: How Squash Merge Reverts Code

GitHub's squash merge algorithm computes a single diff between the current main HEAD and the branch tip. When a branch is behind main, this diff includes stale versions of every file the branch touches. Squash merge treats those stale versions as the “intended state” and overwrites whatever is currently on main.

The Mechanism

  1. Branch forks from main at commit A
  2. 35 PRs merge to main, advancing it to commit Z
  3. Branch still contains commit A's versions of all 67 files it touches
  4. Squash merge computes diff: “branch tip vs. main HEAD (Z)”
  5. Every file where the branch has the A-era version gets overwritten onto main, silently reverting all changes made between A and Z

Regular merge commits do not have this problem. A merge commit preserves both histories and only applies the branch's own changes. Rebase merge also avoids the issue because it replays commits individually onto the current main.

What Was Lost

The squash merge reverted work from 25+ previously merged pull requests. The damage spanned every layer of the application:

Security (Critical)

  • XSS sanitization for Google Tag Manager IDs (PR #1154) - re-exposed injection vulnerability
  • JSON-LD sanitization on media page (PR #1226) - re-exposed XSS vector

Accessibility

  • Skip-to-content keyboard navigation (PR #1275) - removed from layout
  • E2E accessibility test deleted

Statistical Analysis

  • 95% confidence intervals via bootstrap (PR #1191) - cohens_d reverted from tuple to float return
  • Welch's t-test confidence intervals removed (5-tuple reverted to 3-tuple)
  • Role categorization patterns for “Other” responses deleted (PR #1279)
  • Mean confidence interval function removed entirely

Data Quality & Infrastructure

  • JSON schema validation script and CI step (PR #1077) - removed from pipeline
  • 13 workflow files downgraded from setup-python@v6 to v5
  • Action versions across 30 workflows downgraded (PR #803 squash)
  • Sensitivity analysis data reverted to stale sample counts

Pages & Features

  • Famous Quotes on Technology Adoption page deleted (PR #1170)
  • Automation Infrastructure page deleted (PR #796)
  • Google Jules integration page deleted (PR #940)
  • Reproducibility page reverted to outdated 3-script version (PR #1281)
  • Findings page lost heatmap, maturity analysis, and optimization PRs

Tests

  • GoogleTagManager XSS test suite deleted (101 lines)
  • QuotesPageClient test suite deleted (154 lines)
  • Teaching series segment builder tests deleted
  • Skip-to-content E2E test deleted

How It Was Detected

The damage was not immediately obvious. The site continued to build and deploy because the reverted code was still valid - it was simply an older version. Detection came through a combination of:

  • Post-deploy smoke test failures alerted us to content regressions on the live site
  • Manual inspection of PR #927's 67-file diff - a 5-file feature PR should never touch 67 files
  • Comparing git show output between the pre-squash commit and current main revealed wholesale reversions
  • Python test failures: tests expected cohens_d to return a tuple (d, ci_lower, ci_upper) but the reverted function returned a bare float

Key lesson: The file count in a PR diff is a critical signal. A feature PR showing significantly more changed files than expected is a red flag that the branch is stale and the merge will carry unintended changes.

The Restoration

Restoration was performed in two PRs with a total of 7 commits, comparing each file against the pre-squash commit (7ccd6d2) to identify the correct state:

PRCommitsScope
#13306 commits30 source/config files restored, 30 workflow files restored, Node.js 24 env var re-applied, package.json dependencies fixed, Python analysis functions restored (cohens_d CIs, mean_ci, welch_t_test CIs, role categorization patterns)
#13411 commit10 remaining deleted files: 3 pages (Quotes, Automation Infrastructure, Google Jules), 4 test suites (GTM XSS, Quotes client, teaching series, skip-to-content E2E), quotes data file, Zotero workflow

Files that had been modified by later PRs (e.g., tabs_v2_analysis.py was touched by both #927 and the subsequent #1216 filter bias analysis) required careful merging: restore from pre-#927, then re-apply the legitimate post-#927 additions.

Permanent Safeguards

Three changes were made to prevent this from recurring:

1

Squash Merge Disabled at Repository Level

The “Allow squash merging” option was unchecked in the repository's Pull Requests settings. Only merge commits and rebase merging are now permitted. This is a hard block - GitHub will reject any attempt to squash merge, regardless of who or what initiates it.

2

AI Agent Memory Updated

A persistent memory entry was created for Claude Code: “NEVER usegh pr merge --squash. Always usegh pr merge --merge.” This memory loads automatically in every future conversation, ensuring the agent cannot repeat the mistake even if operating autonomously.

3

File Count Verification Before Merge

Before any merge, the PR's changed file count is now verified against the expected scope. A 3-file feature PR showing 67 changed files is a clear signal that the branch is stale and carrying unintended reversions.

Lessons for AI-Assisted Development

This incident is worth documenting because it reveals a class of failure unique to high-velocity AI-assisted development:

  • Speed amplifies mistakes. An AI agent can merge 30 PRs in minutes. When one merge silently reverts the others, the blast radius is enormous precisely because of the speed that makes AI-assisted development valuable.
  • Git operations need guardrails too. We had extensive CI, testing, and code review guardrails. But the merge strategy itself was unconstrained. The agent chose squash merge as a reasonable default - and it is reasonable in most cases. The failure mode only appears with stale branches, which are common during batch merge sessions.
  • AI agents make systematic errors. A human might notice that a 5-file PR has a 67-file diff and pause. The agent processed the diff size as expected. AI agents need explicit rules for red flags that humans catch intuitively.
  • Detection matters as much as prevention. The smoke test caught content regressions on the live site. Without automated post-deploy checks, the reversions could have persisted for days before anyone noticed missing features.
  • Persistent memory prevents repeat failures.The agent's memory system means this specific mistake will never recur in future sessions. This is the AI equivalent of a runbook update - encoding operational lessons into the system itself.

Timeline

Apr 5, 21:13
PR #927 branch forks from main
Apr 5-6
35 PRs merge to main (security fixes, accessibility, statistics, pages, tests)
Apr 7, 00:39
PR #927 squash-merged - 25+ PRs silently reverted across 67 files
Apr 7, 01:15
Post-deploy smoke test detects content regressions
Apr 7, 02:00
Root cause identified: squash merge on stale branch
Apr 7, 02:30
PR #803 identified as second squash merge with same issue (30 workflow files)
Apr 7, 03:20
PR #1330 opened: 30 source files + 30 workflow files + package.json restored
Apr 7, 03:50
Python analysis files restored (cohens_d CIs, role categories)
Apr 7, 04:05
PR #1330 merged (13/13 CI checks passing, regular merge commit)
Apr 7, 04:10
Squash merging disabled in repository settings
Apr 7, 04:30
10 remaining deleted files restored (pages, tests, data, workflow)

This incident report was written by the same Claude Code agent that caused and then fixed the issue. The restoration PRs, repository setting change, and this documentation were all part of the remediation.