anthropics / anthropics/claude-code

[BUG] Stop hook stop-hook-git-check.sh: unpushed-commit check silently passes on branches with no remote ref, and false-positives after a merged PR

Open Beginner friendly
#95,524 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:claude-code-web area:hooks bug has repro
Dominant language
TypeScript
Stars
146k
Forks
23.8k
PR merge metrics
PR metrics pending

Description

Preflight Checklist
  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code
What's Wrong?

The shipped Stop hook ~/.claude/stop-hook-git-check.sh counts unpushed commits with
git rev-list "$upstream..HEAD", where $upstream falls back to origin/HEAD. In a
clone where origin/HEAD is unset — which is the case in Claude Code on the web
containers — that rev-list exits 128, and the hook's || unpushed=0 turns the
failure into "nothing unpushed".

Two bugs, both from that one line:

  1. False negative (the serious one): a branch created with git checkout -b …
    that has real, never-pushed commits produces no warning at all. The hook exists to
    stop work being lost, and this is exactly the shape in which it is lost.

  2. False positive: after a PR is merged and the branch is brought up to the default
    branch (git fetch origin main && git merge --ff-only FETCH_HEAD), HEAD carries
    GitHub's merge commit while origin/$current_branch still points at the pre-merge
    tip. The hook then demands a push for a commit that is already on origin/main.
    Observed four times in one session, each answered by a push that changed nothing.

What Should Happen?
  1. A branch with commits that exist on no remote should produce the warning
    (exit 2), regardless of whether a remote ref for that branch name exists.

  2. A branch whose commits are all reachable from some remote ref should be silent
    (exit 0), even when the branch's own remote ref is behind.

In short: the hook should ask "is this commit on the server anywhere?", not "is this
commit ahead of this branch's own remote ref?" — a commit reachable from any remote
ref cannot be lost, whatever the ref is called.

Error Messages/Logs
$ git rev-parse --is-shallow-repository
true
$ git rev-parse origin/HEAD; echo "rc=$?"
rc=128
$ git rev-list "origin/HEAD..HEAD" --count; echo "rc=$?"
fatal: ambiguous argument 'origin/HEAD..HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
rc=128
$ git rev-list HEAD --not --remotes --count
1
Steps to Reproduce

Both cases run against the shipped hook. Note: git config commit.gpgsign false is
required — with signing enabled, the signing block exits first and the unpushed
counter is never reached.

  1. Set up a repo with a remote:

    d=$(mktemp -d); cd "$d"
    git init -q --bare r.git && git clone -q r.git w && cd w
    git config user.email t@e; git config user.name T; git config commit.gpgsign false
    echo a > f && git add f && git commit -qm a && git branch -M main
    git push -q origin main && git fetch -q origin

  2. FALSE NEGATIVE — new local branch with a real unpushed commit:

    git checkout -q -b feature origin/main
    echo b > g && git add g && git commit -qm "real work"
    echo '{"stop_hook_active": false}' | bash ~/.claude/stop-hook-git-check.sh; echo "exit=$?"

    Expected: exit=2 with a warning. Actual: exit=0, silent.

  3. FALSE POSITIVE — branch brought up to a merged default branch:

    git push -q origin feature && git checkout -q main
    git merge -q --no-ff feature -m "Merge pull request #1" && git push -q origin main
    git fetch -q origin && git checkout -q feature && git merge -q --ff-only origin/main
    echo '{"stop_hook_active": false}' | bash ~/.claude/stop-hook-git-check.sh; echo "exit=$?"

    Expected: exit=0, silent. Actual: exit=2, "1 unpushed commit(s)".

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

No response

Claude Code Version

2.1.278 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Root cause

# lines 41-45
if git rev-parse "origin/$current_branch" >/dev/null 2>&1; then
  upstream="origin/$current_branch"
else
  upstream="origin/HEAD"     # <- not set in these clones
fi

# line 116
unpushed=$(git rev-list "$upstream..HEAD" --count 2>/dev/null) || unpushed=0
#          `- exits 128 when origin/HEAD is unset;  `- failure becomes "0 unpushed"

Proposed fix

-  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 stays as-is for the wording of the two messages.

This is not a new idea: it is the idiom the signing block at line 79 of the same file
already uses —

local_count="$(git rev-list HEAD --not --remotes --count 2>/dev/null)"

— and the comment above it gives the rationale, citing #69586 ("would sweep in
teammates' already-published commits"). The same reasoning applies to the unpushed
counter, which was simply not migrated when that fix landed.

Verification

Shipped vs. patched hook over the same four states:

Case shipped patched
New local branch, no remote ref, 1 real commit exit 0 (silent — bug) exit 2
Branch fast-forwarded onto merged default branch exit 2 (false alarm) exit 0
Real local work on top of a pushed branch exit 2 (counts 2 — includes merge commit) exit 2 (counts 1)
Everything pushed exit 0 exit 0

Note on workarounds

Patching the file in the container does not persist: it is re-provisioned together with
~/.claude/launcher-settings.json. A local edit survived about seven minutes, so users
cannot work around this themselves.

File identity: sha256 b44b87244bec841689b9d7dc864eb28fe05b897a34a09899d808e284b3f8c3ff

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ~/.claude/stop-hook-git-check.sh, reading the upstream selection around lines 41-45, the signing block around line 79, and the unpushed counter at line 116. Run the issue's reproduction cases for a branch without a remote ref, a branch updated from a merged default branch, and fully pushed work; done means the first warns, the second stays silent, and existing signing checks remain unaffected.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash, git
Domain
cli, tooling
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.