[BUG] Stop hook advises rewriting published history: shallow clone + signed GitHub merge commit flagged as "Unverified"
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
~/.claude/stop-hook-git-check.sh repeatedly flagged a GitHub-created merge
commit on published main as Unverified, and advised rewriting it:
There are commit(s) on branch 'main' that GitHub will show as Unverified
(missing signature, or committer email is not noreply@anthropic.com):
2e96012 noreply@github.com
Please run 'git config user.email noreply@anthropic.com && git config user.name Claude',
then 'git commit --amend --no-edit --reset-author' for the tip commit, or
'git rebase --exec "git commit --amend --no-edit --reset-author" 2e960122^' for
earlier commits, then push.
That commit is hours old, already on origin/main, signed by GitHub's own key,
and shown as Verified on github.com. Following the advice would have rewritten
authorship (a human's) and ~15 commits of published history on a shared default
branch.
Two defects combine to produce it:
:83— the predicate flags any commit whose committer email is not
noreply@anthropic.com, regardless of signature. GitHub commits its
server-side merges as GitHub <noreply@github.com> and signs them. So on a
merge-commit workflow this matches every merged PR.
Confirmed the flagged commit is signed:
$ git cat-file commit 2e96012 | sed '/^$/q' | grep -c '^gpgsig'
1
:77— the scope check usesgit rev-list HEAD --not --remotes, which is
unsound on a shallow clone: remote-tracking refs exist but their history is
truncated, so ancestors of origin/main are unreachable from any remote ref
and read as local-only.
$ test -f .git/shallow && echo shallow # shallow
$ git branch -r --contains 2e96012 | wc -l # 0
$ git rev-list HEAD --not --remotes --count # 1 <- a published commit
The in-file comments cite #69586, which fixed a sibling case (the origin/HEAD
fallback). It does not cover this one: origin/$current_branch resolves fine
here; it is the ref's contents that are cut.
What Should Happen?
The hook should not advise history rewriting for commits it cannot prove are
unpublished, and should not flag correctly-signed commits at all.
Specifically:
- Drop the committer-email disjunct at :83. The signature-header test alone is
what GitHub's Verified badge reflects; a commit signed by a different
legitimate key (GitHub's) is not unverified.
- Skip the block entirely when
$(git rev-parse --git-dir)/shallowexists —
--not --remotes cannot answer "is this published?" there. Optionally fall
back to git ls-remote for an authoritative answer.
Verified both fixes locally: with the email disjunct removed, three GitHub merge
commits stop being flagged while a synthetic unsigned commit is still caught.
Independently, git fetch --unshallow alone silences the hook
(local-only 1 -> 0, and git branch -r --contains then reports origin/main).
Error Messages/Logs
Steps to Reproduce
- Start a Claude Code Remote session (clones are shallow by default).
- Work in a repo that uses merge commits, where PRs are merged server-side on
github.com so the merge commit is committed by GitHub <noreply@github.com>.
- Ensure the merge commit is an ancestor of origin/main but outside the
truncated history of the local remote-tracking ref
(git branch -r --contains <sha> returns nothing).
- End a turn so the stop hook runs.
- Observe the Unverified warning and the --reset-author / rebase advice for a
published, signed commit.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.235 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Terminal.app (macOS)
Additional Information
Claude Code Remote (web) session container
git 2.43.0
Harness-configured SSH signing: commit.gpgsign=true, gpg.format=ssh,
user.signingkey=/home/claude/.ssh/commit_signing_key.pub,
user.email=noreply@anthropic.com
Hook: ~/.claude/stop-hook-git-check.sh (shipped; lines :77 and :83)
Broader pattern worth auditing: CCR clones shallow by default, and tooling that
reasons about "is this published?" returns confident wrong negative answers
there rather than reporting that it cannot tell. A second instance in the same
session:
$ git diff --stat main...FETCH_HEAD
(prints nothing — no merge base in truncated history; reads as "no changes"
rather than "cannot compute")
A "cannot determine" path is nearly always safer than defaulting to "nothing
found", especially when the output drives destructive advice.