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
Nobody has claimed this yet.
- 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:
-
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. -
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 whileorigin/$current_branchstill points at the pre-merge
tip. The hook then demands a push for a commit that is already onorigin/main.
Observed four times in one session, each answered by a push that changed nothing.
What Should Happen?
-
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. -
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.
-
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 -
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.
-
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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