worktree-stale-local-main-ref-inflates-pr-diff · v1.2.0 · 2026-08-10 · sha256 2fa875911896caee

worktree-stale-local-main-ref-inflates-pr-diff v1.2.0A

Immutable. This exact content is served forever at /api/v1/blob/2fa875911896caee.

---
name: worktree-stale-local-main-ref-inflates-pr-diff
description: |
  Stop a false "my PR is reverting/touching dozens of upstream files" alarm when
  `git diff main...<branch>` reports far more files than your commit changed, in a
  multi-worktree repo. Use when: (1) you're in a worktree at
  `<repo>/.claude/worktrees/<X>/` (or any `git worktree`), (2) `git diff main...HEAD`
  (3-dot) or `--stat` shows tens/hundreds of files + thousands of lines but your
  actual commit (`git show --stat HEAD`) touched only a handful, (3) you're about to
  panic about the `stale-base-pr-silently-reverts-upstream-content` trap or block your
  own merge. Root cause: the LOCAL `main` ref is stale — it's parked many commits
  behind `origin/main` (often checked out in the PRIMARY worktree, which nobody pulled),
  so the merge-base of (local main, your branch) is ancient and the 3-dot diff includes
  every file that landed on origin/main since. The REAL PR diff is against `origin/main`.
  Verify with `git diff origin/main...HEAD --stat` (after `git fetch`) and
  `gh pr view <N> --json files`. Sibling of git-diff-2dot-vs-3dot-merge-safety
  (operator semantics) and worktree-outer-ls-mistaken-for-main-state (stale state via ls).
  Also fires when a CODE-REVIEW SUBAGENT claims your PR "carries unrelated code / a
  subsystem you didn't author" — same stale-local-main root cause; the authoritative
  check is `gh pr diff <N> --name-only` (the reviewer likely read the full-file context
  and mistook already-merged code for this PR's hunks).
author: Claude Code
version: 1.2.0
date: 2026-06-23
disable-model-invocation: true
---

# Worktree: a stale local `main` ref inflates `git diff main...branch`

## Problem

In a multi-worktree repo, `git diff main...<your-branch>` (or `--stat`) reports
**far more files and lines than your commit actually changed** — e.g. 92 files /
8,000+ insertions when your one commit touched 6 files. The instinct is to panic
that your PR is silently reverting or re-touching a pile of upstream work (the
`stale-base-pr-silently-reverts-upstream-content` trap) and to block your own
merge or demand a rebase. Almost always, nothing is wrong with the PR — the
**local `main` ref is stale**.

## Context / Trigger Conditions

All of:
- You're working inside a git worktree (`git rev-parse --is-inside-work-tree`;
  path contains `.claude/worktrees/<name>/` or `git worktree list` shows 2+ paths).
- `git diff main...HEAD --stat` (3-dot) shows tens/hundreds of files, but
  `git show --stat HEAD` (your actual commit) shows only a few.
- You branched off what you *thought* was current `main`, and `gh pr view <N>`
  reports the PR as `mergeable: MERGEABLE / mergeStateStatus: CLEAN`.

Why it happens: `main...branch` (3-dot) diffs from the **merge-base** of the two
refs. The merge-base uses your **LOCAL** `main` ref. In a multi-worktree setup the
local `main` ref is whatever the **primary worktree** last checked out / pulled —
frequently many commits behind `origin/main` because sibling sessions merge via
GitHub (squash) and nobody runs `git pull` on the parked primary checkout. So the
merge-base is ancient, and the 3-dot diff includes every file that landed on
`origin/main` since that old point — none of which your branch is actually
reverting. (This is the REF being stale, distinct from
`git-diff-2dot-vs-3dot-merge-safety`, which is about 2-dot-vs-3-dot operator
semantics against a *correct* `origin/main`.)

## Solution

Diff against the **remote** ref, not the local `main` ref:

```bash
git fetch origin main -q

# The TRUE PR diff (what GitHub will merge):
git diff origin/main...HEAD --stat

# Confirm divergence is small:
git rev-list --count HEAD..origin/main   # commits origin/main has, your branch doesn't
git rev-list --count origin/main..HEAD   # YOUR commits

# Is your branch's base actually on origin/main's history? (necessary, NOT sufficient
# — see the Verification note below; this does not rule out a revert on its own)
git merge-base --is-ancestor <your-branch-base-sha> origin/main && echo "clean base"

# The check that DOES rule out a revert — deletions, from the merge-base:
git diff --diff-filter=D --name-only origin/main...HEAD   # empty ⇒ deletes nothing

# Ground truth from GitHub — the actual file list the PR will change:
gh pr view <N> --json files -q '.files[].path'
gh pr view <N> --json mergeable,mergeStateStatus
```

If `git show --stat HEAD` matches `gh pr view <N> --json files` (your handful of
files) and the PR is `MERGEABLE / CLEAN`, the 92-file `main...branch` number was a
pure local-ref artifact — **proceed**. Do not rebase, do not block.

**Don't bother fixing the local `main` ref** from inside the worktree — you usually
can't (`git checkout main` fails: "main is already used by worktree at <primary>").
Just diff against `origin/main`; that's the ref that matters for the PR anyway.

## Verification

- `git show --stat HEAD` file count == `gh pr view <N> --json files` count.
- `git diff origin/main...HEAD --stat` shows only your files (small).
- `gh pr view <N>` → `mergeStateStatus: CLEAN`.
- `git diff --diff-filter=D --name-only origin/main...HEAD` is empty (or lists only
  files you meant to delete). **This is the check that proves no revert.**
- `git merge-base --is-ancestor <base-sha> origin/main` returns 0 — your base is on
  origin/main's history. Useful, but **it does not prove the merge won't revert
  upstream**, and treating it as proof is the mistake. A branch whose POINTER was
  moved onto current main while its TREE stayed old satisfies this condition exactly
  — 0 commits behind, ancestor check clean — and still records deleting every file
  that landed in between (`pr-from-stale-branch-silently-reverts-newer-main-files`,
  "the harder variant"). Only the `--diff-filter=D` line above closes that gap.

## Example

Worktree `chatbot`, one feature commit touching 6 files. `git diff main...HEAD --stat`
→ **92 files, 8,381 insertions** (TM v2/v3 + v7 analysis from other sessions). Alarm:
"is my PR reverting all of that?" Checks:
- `git show --stat HEAD` → exactly 6 files. ✓
- `git fetch origin main` then `git rev-list --count HEAD..origin/main` → **1** (just
  the sibling docs commit that added my own prompt file), `origin/main..HEAD` → 1.
- `git merge-base --is-ancestor 9fd95c95 origin/main` → 0 (clean ancestor).
- `gh pr view 1019 --json files` → the same 6 files; `mergeStateStatus: CLEAN`.

Conclusion: the 92-file number was the **stale local `main` ref** (parked far behind
origin/main in the primary worktree). PR was clean. Merged without rebase.

## Notes

- The cheap falsification is always `git show --stat HEAD` vs `gh pr view <N> --json
  files`. If those two agree and are small, the 3-dot `main...branch` number is noise.
- `git fetch` first — without it, even `origin/main` is whatever you last fetched.
- This is the *inverse* worry of a real stale-base revert: here the diff OVER-reports
  (local base too OLD); the real revert trap (`stale-base-pr-silently-reverts-upstream-content`)
  is when your branch's content actually overlaps and overwrites a merged upstream
  edit. Distinguish by checking `gh pr view --json files` (does it list the upstream
  files?) + reading the actual hunks, not the 3-dot file count.

## Variant — a code-review subagent flags "unrelated code riding along"

Same root cause, different messenger. A dispatched **code-reviewer** ran `gh pr diff <N>`
on a 1-commit PR (resolver fix touching 2 files) and reported that the PR *also* carried
"a conversation-export trace subsystem you didn't review" — because it read the **full
file** `chatbox_tools.py` (whose trace code was already on `origin/main` from prior merged
PRs) and conflated that ambient context with the PR's hunks. The author's own
`git diff main...branch` (stale local main) seemed to corroborate it.

Don't act on the reviewer's scope claim — falsify it first:
```bash
git fetch origin main -q
git log origin/main..<branch> --oneline      # how many commits the PR really has
gh pr diff <N> --name-only                    # AUTHORITATIVE file list GitHub will merge
gh pr diff <N> | grep -E '^\+def |^\+class '  # the actual ADDED hunks, not file context
```
If `--name-only` lists only your files and `git log origin/main..<branch>` is your single
commit, the "riding along" subsystem is pre-existing code the reviewer read for context —
proceed. The reviewer's *substantive* findings on your real hunks still stand; only its
**scope** claim is the artifact.

## See also

- `git-diff-2dot-vs-3dot-merge-safety` — 2-dot vs 3-dot operator semantics (assumes a
  correct `origin/main`); this skill is about the local `main` *ref* being stale.
  **The two are a pair and neither is safe alone:** switching to 3-dot fixes the
  operator and does nothing about the ref, so `git diff main...HEAD` against a stale
  local `main` still over-reports. Fetch, then name `origin/main`.
- `pr-from-stale-branch-silently-reverts-newer-main-files` — the case where the
  deletions are REAL rather than an artifact (a stale TREE under a current pointer).
  That is the one the ancestor check above cannot see.
- `worktree-outer-ls-mistaken-for-main-state` — same stale-local-state family, via `ls`.
- `stale-base-pr-silently-reverts-upstream-content` — the genuine revert trap (the
  thing this false alarm makes you fear); rule it out via the file list + hunks.
- `gh-pr-merge-worktree-checkout-trap` / session-handoff note — why `git checkout main`
  / `--delete-branch` fail from a worktree.