Stop hook reports false "unpushed commits" after a PR merge when the head branch is auto-deleted

Status Open
Reported on v2.1.221
Maintainer reply None cached
Activity 2 comments · opened Aug 4, 2026

Component: Claude Code Remote — ~/.claude/stop-hook-git-check.sh (provisioned by the launcher, registered in ~/.claude/launcher-settings.json)

Also posted as a comment on #83490, which reports the same root cause from a different trigger. Filing standalone because the trigger, the measurements, and the affected line are distinct — dedupe deliberately if you'd rather track it there.

The fix already exists in this file — it was applied to one of two call sites

The signature-check block ~20 lines above the unpushed check already uses the correct idiom, and its comment explains exactly why it avoids $upstream..HEAD:

Scope to commits that are on NO remote ref ($upstream..HEAD would sweep in teammates' already-published commits whenever this branch was cut from a feature branch and has no same-named remote yet — anthropics/claude-code#69586).

The unpushed-commit count at stop-hook-git-check.sh:116 never received the same treatment:

-  unpushed=$(git rev-list "$upstream..HEAD" --count 2>/dev/null) || unpushed=0
+  unpushed=$(git rev-list HEAD --not --remotes --count 2>/dev/null) || unpushed=0

$upstream is still used for message wording, so the two distinct messages ("on branch X" vs "and no remote branch") are unaffected.

Summary

The unpushed-commit check counts $upstream..HEAD, where $upstream is origin/$current_branch if that ref resolves locally. That ref goes stale in a completely ordinary way: you merge a PR, GitHub deletes the head branch ("Automatically delete head branches"), and the local remote-tracking ref origin/<branch> survives pointing at the pre-merge tip. The merge commit is then counted as unpushed even though origin/main already contains it.

The hook then asks the agent to push — which would recreate a branch identical to main with no PR behind it, and since auto-delete only fires on merge, that branch lingers until someone removes it by hand.

Reproduction

  1. Branch from main, commit, push, open a PR.
  2. Merge the PR (API or UI) with Automatically delete head branches enabled on the repo.
  3. git fetch origin main — note this does not prune origin/<branch>, because the refspec is scoped to main.
  4. Stay on the local branch (or re-create it at the merge commit) and let the Stop hook run.

Observed: There are 1 unpushed commit(s) on branch '<branch>'. Please push these changes to the remote repository.

Expected: silence — the commit is on origin/main.

Evidence

Measured on a real repo where HEAD was byte-identical to origin/main:

HEAD                                        = d72387c8
origin/<deleted-branch> (stale)             = b0897ddc   # pre-merge tip

git rev-list b0897ddc..HEAD --count         = 1   # what the hook counted
git rev-list HEAD --not --remotes --count   = 0   # correct: HEAD is on origin/main

git log origin/main..HEAD was empty and git status -sb showed no ahead/behind markers at the same moment.

Root cause

$upstream..HEAD asks "are these commits missing from one specific ref?" The safe question is "are these commits absent from every remote ref?"

Verified

Patched locally and exercised both directions:

  • HEAD == origin/main with a stale origin/<deleted-branch> present → silent, exit 0 (was: exit 2)
  • A genuinely unpushed commit on a fresh local branch → still reports, exit 2

Why this is worth fixing now

It gets more frequent as "Automatically delete head branches" adoption grows, which is the recommended setting for repos where agents open PRs. Every agent PR that merges and self-deletes leaves this trap behind — and the hook's advice (push) is actively counterproductive, producing stray branches that no auto-delete will clean up.

Environment

  • Version: 2.1.221 (Claude Code)
  • Platform: Claude Code on the web / Claude Code Remote (cloud container)
  • Linux 6.18.5-fc-v18, git 2.43.0

Related

  • #83490 — same file and root cause from a different trigger; its proposed diff covered both call sites, and this is the half that didn't land.
  • #69586 — cited by the hook's own inline comment as the reason the signature block moved off $upstream..HEAD. Closed as stale, though the fix shipped.
  • #82624 — adjacent: a missing origin/<branch> tracking ref treated as "not pushed".

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗