gh-pr-merge-unstable-state-needs-auto-and-watch-branch-deletes · v1.0.0 · 2026-05-29 · sha256 99024d11e96bbdf3

gh-pr-merge-unstable-state-needs-auto-and-watch-branch-deletes v1.0.0A

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

---
name: gh-pr-merge-unstable-state-needs-auto-and-watch-branch-deletes
description: |
  Diagnose and recover from two adjacent `gh pr merge` failure modes that masquerade as merge
  conflicts. Use when: (1) `gh pr merge --squash` (or `--merge`) errors with "To have the pull request
  merged after all the requirements have been met, add the `--auto` flag" AND "Run the following to
  resolve the merge conflicts locally" even though you just pushed a clean resolution; (2)
  `gh pr view <N> --json mergeable,mergeStateStatus` returns `MERGEABLE` + `UNSTABLE` rather than
  `MERGEABLE` + `CLEAN`; (3) you delete the remote branch (`git push origin --delete <branch>`)
  immediately after `gh pr merge` returns a conflict warning thinking the merge succeeded — and
  discover the PR has flipped to `CLOSED` rather than `MERGED`. The actual root cause for (1)/(2) is
  almost always pending CI / branch-protection checks, NOT real merge conflicts; the fix is `--auto`
  flag so `gh` queues the merge for when checks pass. For (3) — recovery requires restoring the remote
  branch (`git push -u origin <branch>` from your local copy) and `gh pr reopen <N>` before retrying.
  Sibling to `gh-pr-merge-worktree-checkout-trap` (different failure mode — worktree holding main).
author: Claude Code
version: 1.0.0
date: 2026-05-19
---

# `gh pr merge` UNSTABLE state + branch-delete recovery

## Problem

Two adjacent failure modes that read as "merge conflicts" but aren't:

### Failure A: `gh pr merge` rejects with "conflicts" when there are none

You push a clean resolution of merge conflicts, GitHub shows the PR as MERGEABLE, and yet:

```
$ gh pr merge 915 --squash
To have the pull request merged after all the requirements have been met,
add the `--auto` flag.
Run the following to resolve the merge conflicts locally:
  gh pr checkout 915 && git fetch origin main && git merge origin/main
```

The first line is the actual hint; the second is misleading boilerplate. The real cause is
`mergeStateStatus: UNSTABLE` — branch protection or CI checks are pending. `gh` can't merge yet
because the repo's rules aren't satisfied, not because of file-level conflicts.

### Failure B: PR auto-closes if you delete the remote branch right after Failure A

Common pattern: previous PR on the same branch merged cleanly with `gh pr merge <N> --squash`. You
then do `git push origin --delete <branch>` to clean up. Habit kicks in for the next PR — you run
`gh pr merge` (gets Failure A above), notice the "conflicts" warning, run
`git push origin --delete <branch>` anyway thinking the merge already landed remotely...

```
$ gh pr view 915 --json state
{"state":"CLOSED"}   # ← not MERGED — the PR closed because the branch was deleted with the merge un-queued
```

GitHub treats a deleted-branch PR with no merge in progress as abandoned and closes it.

## Context / Trigger conditions

- `gh pr merge --squash` (or `--merge`) returns with the "add the `--auto` flag" / "resolve
  conflicts" stderr
- `gh pr view <N> --json state,mergeable,mergeStateStatus` shows `{state: OPEN, mergeable: MERGEABLE,
  mergeStateStatus: UNSTABLE}` — the `UNSTABLE` is diagnostic; it means "no real conflicts, but checks
  aren't passing yet"
- Or: a PR you just tried to merge is now `state: CLOSED` rather than `state: MERGED`, AND the remote
  branch is missing
- Repo has branch protection rules (required status checks, required reviews) OR CI workflows that
  haven't completed yet
- You're in a multi-PR session where the previous PR merged cleanly, conditioning you to expect the
  same flow

## Root cause

**For Failure A:** `gh pr merge` (without `--auto`) requires the PR to be fully ready to merge RIGHT
NOW. If branch-protection rules or CI checks are pending, `gh` reports it as a generic merge problem
with misleading conflict-resolution advice. The actual diagnostic is the
`mergeStateStatus` field — `UNSTABLE` means "pending checks", `BLOCKED` means "failed checks or
missing reviews", `CLEAN` means "ready to merge", `DIRTY` means "real file conflicts".

**For Failure B:** GitHub's UI treats "branch deleted + no merge in progress" as PR abandonment. The
PR closes; the merge never happens; recovery requires restoring the branch AND reopening the PR.

## Solution

### Diagnose first — three-line state check

Before retrying or panicking:

```bash
gh pr view <N> --json state,mergeable,mergeStateStatus,statusCheckRollup
```

Interpretation:

| `state` | `mergeable` | `mergeStateStatus` | Meaning | Action |
|---|---|---|---|---|
| OPEN | MERGEABLE | CLEAN | Ready to merge | `gh pr merge <N> --squash` |
| OPEN | MERGEABLE | UNSTABLE | Pending CI / checks | `gh pr merge <N> --squash --auto` |
| OPEN | MERGEABLE | BLOCKED | Failed checks or missing required reviews | Fix the failed check or request review |
| OPEN | CONFLICTING | DIRTY | Real file conflicts | Resolve locally, push, retry |
| CLOSED | (any) | (any) | PR closed (often: branch deleted) | Restore branch + reopen (see recovery below) |
| MERGED | (any) | (any) | Already done | Nothing to do |

### Fix Failure A — use `--auto`

```bash
gh pr merge <N> --squash --auto
# Returns silently; merge queues for when checks pass; GitHub auto-deletes remote branch on merge
```

`--auto` tells GitHub to wait for branch-protection requirements and then merge automatically. No
polling required — GitHub handles it. The remote branch is deleted by GitHub at merge time (no need
to do it yourself).

### Fix Failure B — restore branch + reopen PR + retry

```bash
# Your local branch still has the commits — push it back up
git push -u origin <branch-name>

# Reopen the PR (gh closed it when the branch went away)
gh pr reopen <N>

# Verify it's back
gh pr view <N> --json state,mergeable,mergeStateStatus

# Then merge with --auto
gh pr merge <N> --squash --auto
```

If you've already discarded the local branch (e.g., `git branch -D` after the wrong-time delete),
recovery is harder — you may need to recover commits via `git reflog` or recreate from a colleague's
clone.

## Verification

After applying `--auto`:

1. `gh pr view <N> --json state` should still show `OPEN` immediately after the command
2. Within minutes (or however long your CI takes), GitHub will merge automatically
3. Final state: `gh pr view <N> --json state,mergedAt` shows `{state: MERGED, mergedAt: <timestamp>}`
4. Remote branch is auto-deleted by GitHub
5. You receive no email failure notification (a clean signal)

## Example

Session ending PR #915 (a docs handoff), 2026-05-19:

```
$ gh pr merge 915 --squash
To have the pull request merged after all the requirements have been met, add the `--auto` flag.
Run the following to resolve the merge conflicts locally:
  gh pr checkout 915 && git fetch origin main && git merge origin/main

$ gh pr view 915 --json state,mergeable,mergeStateStatus
{"mergeable":"MERGEABLE","mergeStateStatus":"UNSTABLE","state":"OPEN"}
                                  ^^^^^^^^
                                  not DIRTY → no real conflicts; pending checks

$ gh pr merge 915 --squash --auto
# (silent success — queues for CI)
```

(Earlier in the same session, the same orchestrator hit Failure B on the same PR by running
`git push origin --delete docs/s207-handoff` immediately after `gh pr merge` returned the conflict
warning — the PR flipped to CLOSED. Recovered with `git push -u origin docs/s207-handoff` +
`gh pr reopen 915` + `gh pr merge 915 --squash --auto`.)

## Notes

- **Don't reflexively delete the remote branch after `gh pr merge` errors.** The merge may not have
  landed. Check `gh pr view <N> --json state,mergedAt` first; only delete if `mergedAt` is non-null.
- **`--auto` is safe to use even when checks pass immediately.** If the PR is already CLEAN, `--auto`
  merges instantly. No downside to using it as the default.
- **Branch-protection diagnostics:** if `mergeStateStatus` stays `UNSTABLE` for >5min after a push,
  check `gh pr checks <N>` to see which check is hanging. A common cause is a `paths`-filter workflow
  that didn't trigger because the PR touched no matching paths — that check stays "pending" forever
  and blocks merge until the rule is updated.
- **Sibling failure mode:** `gh-pr-merge-worktree-checkout-trap` — when `gh pr merge --delete-branch`
  fails with `fatal: 'main' is already used by worktree at ...`. That's a LOCAL-side failure of the
  post-merge branch checkout; the GitHub merge itself succeeds. Different beast from the failures
  documented here — verify by checking `gh pr view <N> --json mergedAt`.
- **Required-status-check repos:** in repos with branch protection requiring specific status checks,
  `--auto` is essentially mandatory — no human can merge instantly if checks aren't done. Build the
  habit project-wide.

## References

- [gh pr merge — official docs](https://cli.github.com/manual/gh_pr_merge)
- [GitHub mergeStateStatus enum reference](https://docs.github.com/en/graphql/reference/enums#mergestatestatus)
- Sibling skill: `~/.claude/skills/gh-pr-merge-worktree-checkout-trap/SKILL.md`