stacked-pr-base-branch-deletion-auto-closes-dependent · v1.0.0 · 2026-05-13 · sha256 fd8b947b239712d9

stacked-pr-base-branch-deletion-auto-closes-dependent v1.0.0A

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

---
name: stacked-pr-base-branch-deletion-auto-closes-dependent
description: |
  Recover from the trap where deleting a base PR's branch (via `gh api -X DELETE
  refs/heads/<branch>`) auto-closes any open dependent stacked PR, and the closed
  PR cannot be reopened or retargeted. Use when: (1) you set up a stacked PR
  pair (PR2's `base` field = PR1's branch instead of `main`), (2) you merged PR1
  via squash, (3) you then deleted PR1's remote branch via the API (because
  `gh pr merge --delete-branch` failed locally with the worktree-checkout-trap
  and you used the API as a workaround), (4) the dependent PR2 is now reported
  as `state: CLOSED` even though you never closed it, (5) `gh pr reopen N` fails
  with `Could not open the pull request`, (6) `gh pr edit N --base main` fails
  with `Cannot change the base branch of a closed pull request`. The only
  recovery is to open a fresh PR from the same head branch with base=main.
  Sister skill to `gh-pr-merge-worktree-checkout-trap` (the upstream cause).
author: Claude Code
version: 1.0.0
date: 2026-05-08
---

# Stacked PR: Base-Branch Deletion Auto-Closes the Dependent PR

## Problem

You have a stacked PR pair:

- **PR1** — `base: main`, `head: feature-pr1`
- **PR2** — `base: feature-pr1`, `head: feature-pr2` (stacked on PR1)

You squash-merge PR1. The merge succeeds. You then run:

```bash
gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/feature-pr1
```

…to clean up the orphaned remote branch (because `gh pr merge --delete-branch`
failed locally with the worktree-checkout-trap and you used the API as a
workaround). Within seconds, PR2 silently transitions to `state: CLOSED`:

```bash
$ gh pr view <PR2> --json state,closed,mergedAt,baseRefName
{"baseRefName":"feature-pr1","closed":true,"mergedAt":null,"state":"CLOSED"}
```

You never closed PR2. The PR's `head` branch still exists with all the work.
But:

```bash
$ gh pr reopen <PR2>
API call failed: GraphQL: Could not open the pull request. (reopenPullRequest)

$ gh pr edit <PR2> --base main
GraphQL: Cannot change the base branch of a closed pull request. (updatePullRequest)
```

GitHub refuses to reopen the PR (its base branch no longer exists), and
refuses to retarget a closed PR. The PR is effectively a dead reference —
all comments, reviews, and history land on a record that nothing can resurrect.

## Why this happens

When a PR's base branch is deleted, GitHub's behavior is asymmetric:

- **If the base branch is deleted via the GitHub UI's "delete branch" button
  on a merged PR**, GitHub usually auto-retargets dependent open PRs to the
  default branch (typically `main`).
- **If the base branch is deleted via the Git Refs API directly**
  (`DELETE /repos/.../git/refs/heads/<branch>`), GitHub does NOT auto-retarget.
  Instead, it auto-closes any open PR whose `base` field referenced the now-
  missing branch.

The closed PR cannot be reopened because GitHub validates that the base ref
exists at reopen time. It cannot be retargeted because the reopen happens
before the retarget — both are blocked by the missing-base validation.

## Context / Trigger Conditions

You are in this trap if **all** of these are true:

1. You used a stacked PR setup (PR2 explicitly opened with `--base <PR1-branch>`,
   not `main`).
2. PR1 squash-merged (PR1 is in `state: MERGED`).
3. You deleted PR1's remote branch via `gh api -X DELETE` (the API), not
   via `gh pr merge --delete-branch` (which would have done it through
   GitHub's UI flow that handles stacked retarget).
4. PR2 is now `state: CLOSED` with `mergedAt: null` and `closed: true`.
5. `gh pr reopen <PR2>` fails with `Could not open the pull request`.

## Solution

The dead PR cannot be recovered. Open a fresh PR from the same head branch
against `main`:

```bash
# 1. Verify the head branch still exists with your work
git fetch origin
git ls-remote origin refs/heads/feature-pr2
# should print a SHA — your work survives on the remote branch

# 2. (Optional) Verify HEAD diff against main is what you expect
git log origin/main..origin/feature-pr2 --oneline | head

# 3. Open a fresh PR with base=main; reuse the original PR's body
#    (you can copy-paste from the closed PR; comments don't migrate).
gh pr create \
  --base main \
  --head feature-pr2 \
  --title "feat(...): same title as the dead PR" \
  --body "$(cat <<'EOF'
Replaces #<DEAD_PR_NUMBER> (auto-closed when PR1's base branch was deleted
post-merge — GitHub couldn't auto-retarget). Same diff, now based on main
directly.

[... rest of original body ...]

Code-review findings preserved as comments on the closed PR:
https://github.com/<owner>/<repo>/pull/<DEAD_PR_NUMBER>#issuecomment-<ID>
EOF
)"

# 4. Squash-merge the new PR as usual
gh pr merge <NEW_PR> --squash
```

Reviews and comments on the dead PR are NOT auto-migrated. Leave a comment
on the new PR that links to the dead PR's review thread so the audit trail
stays connected.

## Prevention (recommended workflow)

Pick ONE of these patterns when working with stacked PRs + worktrees:

### Pattern A — Merge both PRs before any branch cleanup

Don't delete PR1's branch until PR2 is also merged. After PR1 squash-merges,
GitHub automatically marks PR1's branch as "ready to delete" but doesn't
delete it. Leave it alone. Merge PR2 (which still has `base: PR1-branch`).
Once both are merged, clean up:

```bash
gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/feature-pr1
gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/feature-pr2
```

This is the cheapest and safest pattern. PR2 doesn't need to be retargeted —
it merges with `base: feature-pr1`, and GitHub computes the diff correctly
(PR2's head minus PR1's head, which is just PR2's specific commits).

### Pattern B — Retarget PR2 to main BEFORE deleting PR1's branch

If you need to delete PR1's branch immediately after merge (e.g., automation
constraints):

```bash
# 1. Squash-merge PR1
gh pr merge <PR1> --squash

# 2. Retarget PR2 to main (works because PR2 is still OPEN at this point)
gh pr edit <PR2> --base main

# 3. NOW it's safe to delete PR1's branch
gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/feature-pr1
```

The retarget rebases PR2's diff against `main` (which now contains PR1's
squash). PR2 may then need a manual rebase or merge of `main` to resolve
conflicts, but the PR stays open and reviewable throughout.

## Verification

After recovery, verify:

```bash
# Old PR is closed-unmerged (intentional dead state)
gh pr view <DEAD_PR> --json state,mergedAt
# {"state":"CLOSED","mergedAt":null}

# New PR is open against main with the expected diff
gh pr view <NEW_PR> --json state,baseRefName,headRefName
# {"state":"OPEN","baseRefName":"main","headRefName":"feature-pr2"}

# New PR's diff matches what you intended (no PR1 commits leaked back in)
gh pr diff <NEW_PR> | head -50
```

## Notes

- **The harness/CLI does not warn before this happens.** `gh api -X DELETE`
  on a refs/heads/X executes immediately and the auto-close cascade is a
  GitHub-server-side reaction that lands within ~1 second. There is no
  "this will close N dependent PRs, continue?" prompt.
- **The auto-close on dependent PRs is asymmetric with the UI delete-branch
  button.** If you use the GitHub UI's "Delete branch" button on a merged
  PR's page, GitHub generally auto-retargets dependent open PRs to the
  default branch instead of closing them. Only the API-direct ref delete
  triggers the close cascade.
- **GitHub does NOT silently reopen** dependent PRs even if you re-create
  the deleted ref afterwards (`gh api -X PUT refs/heads/feature-pr1`). The
  closed state is sticky.
- **Sister skill — upstream cause:** `gh-pr-merge-worktree-checkout-trap`
  documents why operators reach for `gh api -X DELETE` in the first place
  (because `gh pr merge --delete-branch` fails locally when another worktree
  has `main` checked out). That skill says "the merge succeeded; verify
  state=MERGED and move on" — this skill extends that advice with: if you
  also need to clean up the branch AND a stacked PR depends on it, follow
  Pattern A or B above instead of using the API delete directly.
- **Sister skill — different problem:** `pr-followup-commit-stranded-after-squash`
  covers commits pushed after PR squash-merge — different failure mode
  (stranded commits vs auto-closed PR).

## Example — 2026-05-08 chatbox session

Two-PR stack:
- PR #456 (chatbox hardening), `base: main`, `head: worktree-chatbox-with-data`
- PR #461 (chatbox knowledge gap), `base: worktree-chatbox-with-data`,
  `head: worktree-chatbox-with-data-pr2`

Sequence that hit the trap:

1. PR #456 squash-merged (commit `36111e42` on main). ✓
2. `gh pr merge 456 --squash --delete-branch` — local-side failed with
   "fatal: 'main' is already used by worktree at '...wow-action'"
   (the worktree-checkout-trap; merge succeeded on GitHub, only local
   cleanup failed).
3. To clean up the orphaned remote PR1 branch, ran:
   `gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/worktree-chatbox-with-data`
4. **Within seconds, PR #461 transitioned to `state: CLOSED`.** No notification.
5. `gh pr reopen 461` → `Could not open the pull request`.
6. `gh pr edit 461 --base main` → `Cannot change the base branch of a closed PR`.
7. **Recovery:** opened PR #465 with `--base main --head worktree-chatbox-with-data-pr2`,
   reused the body, linked the review-comment thread on the dead #461.
   PR #465 squash-merged successfully.

Total recovery time: ~5 minutes. Could have been zero with Pattern A
(don't delete PR1's branch until PR2 is also merged).

## References

- GitHub Git Refs API delete behavior:
  https://docs.github.com/en/rest/git/refs#delete-a-reference
- GitHub stacked-PR documentation (auto-retarget on UI delete):
  https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request
- Sister skill: `~/.claude/skills/gh-pr-merge-worktree-checkout-trap/SKILL.md`