Claude Code on the web: Stop hook (stop-hook-git-check.sh) flags entire branch history as Unverified when local branch diverges from upstream
Summary
The managed Stop hook in the Claude Code on the web image (CCR cloud_default / sandbox-ccr-default), ~/.claude/stop-hook-git-check.sh, can flag dozens of already-published commits authored by OTHER people as "Unverified" and instruct the agent to git commit --amend --reset-author / git rebase --exec ... reset-author across all of them, i.e. a destructive history rewrite of teammates' commits.
Root cause
The hook scopes its signature / committer-email check (and the unpushed-commit count) to the range "$upstream..HEAD":
unverifiable=$(git log --format='%h %G? %ce' "$upstream..HEAD" 2>/dev/null | awk '$2 == "N" || $3 != "noreply@anthropic.com"')
...
unpushed=$(git rev-list "$upstream..HEAD" --count 2>/dev/null)
When the local branch has diverged from (or is unrelated to) the upstream tip, "$upstream..HEAD" expands to EVERY commit reachable from HEAD but not from the upstream, which includes history already published by other contributors. The hook then lists all of them and tells the user to reset-author / rebase the lot.
Repro
- In a cloud session, end a turn while checked out on a local branch whose history has diverged from its upstream. A clean way to hit it: after the remote default branch is fast-forwarded along a different line of history (e.g. you push a feature branch to main via
git push origin feature:main), check out the now-stale localmain. Localmainandorigin/mainno longer share an ancestor. - The Stop hook fires and prints "There are commit(s) on branch 'main' that GitHub will show as Unverified ..." followed by ~50 commits authored by teammates, and recommends
git rebase --exec "git commit --amend --no-edit --reset-author" origin/main.
Following that advice would rewrite and re-author other people's commits and force-push the default branch.
Suggested fix
Anchor the checks on the merge-base instead of the upstream tip, and bail when there is no common ancestor:
base=$(git merge-base "$upstream" HEAD 2>/dev/null)
if [[ -z "$base" ]]; then
# Unrelated histories: cannot identify which commits are new here.
exit 0
fi
range="$base..HEAD"
# use "$range" for both the unverifiable check and the unpushed count,
# and reference "$base" (not "$upstream") in the rebase --exec hint.
This limits inspection to commits genuinely created locally on top of the common ancestor, so already-published history (and other people's commits) is never flagged or proposed for rewrite. In the normal "branch ahead of its upstream" case, merge-base == upstream, so behavior is unchanged.
Environment
- Claude Code on the web, CLAUDE_CODE_REMOTE_ENVIRONMENT_TYPE=cloud_default, image sandbox-ccr-default.
- Claude Code version 2.1.183.
- Hook file:
~/.claude/stop-hook-git-check.sh, provisioned from the base image at session start and wired via~/.claude/launcher-settings.json. Because it is re-provisioned each session, a local edit does not persist, so the fix needs to land in the image.