[BUG] stop-hook-git-check.sh false-positives on SSH-signed commits (%G?=N), creating an unbreakable Stop-hook loop
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?
Environment
- Claude Code on the web (Anthropic-managed cloud session, research preview)
- Hook:
~/.claude/stop-hook-git-check.sh(provisioned by the web launcher via~/.claude/launcher-settings.json) - Signing config in the session:
gpg.format=ssh,commit.gpgsign=true,
user.signingkey=~/.ssh/commit_signing_key.pub, gpg.ssh.program=/tmp/code-sign
Summary
The Stop hook flags validly signed commits as "Unverified" and enters an
infinite loop: it fires on every turn, and the remedy it prescribes
(git commit --amend --no-edit --reset-author) can never satisfy it. The only
escape is pushing the branch.
Root cause
stop-hook-git-check.sh line ~54:
unverifiable=$(git log --format='%h %G? %ce' "$upstream..HEAD" \
| awk '$2 == "N" || $3 != "noreply@anthropic.com"')
The comment above it (lines ~51–52) assumes that signed-but-locally-unverifiable
commits report %G? of U/B/E, so treating N as "unsigned" is claimed to
be a reliable presence check. That assumption is false in this environment.
In the web session, SSH-signed commits report %G? = N, identical to
unsigned, because local verification is impossible:
gpg.ssh.allowedSignersFileis not configured, and- the public key file
~/.ssh/commit_signing_key.pubis empty (0 bytes), and - the signer
/tmp/code-signis sign-only (error: ... only SSH-style signing (-Y sign) is supported).
So git can't verify, printserror: gpg.ssh.allowedSignersFile needs to be configured and exist, and
reports N. The commits are genuinely signed — git cat-file commit HEAD shows agpgsig -----BEGIN SSH SIGNATURE----- header and committer ... <noreply@anthropic.com>.
Actual
Signed commits are flagged identically to unsigned ones; the amend remedy is a
no-op against the check; the hook loops every turn until the branch is pushed
(which empties origin/<branch>..HEAD and is the only thing that silences it).
Suggested fixes (any one)
- Detect signature presence without relying on
%G?— e.g. check for a
gpgsig header directly (git cat-file commit <sha> / git log --format=%GK),
so a real signature isn't read as N.
- Provision
gpg.ssh.allowedSignersFilefrom the signing public key so%G?
can actually verify — but note the key file is currently empty, so that needs
fixing too.
- When local verification is known-unavailable in this runtime, skip the
signature branch and rely only on committer-email + unpushed checks.
Impact
Wasted turns/tokens on a remedy that cannot work, and a confusing UX: the user is
repeatedly told their (correctly signed) commits are unverified.
What Should Happen?
Expected
A commit that carries a valid signature and committer noreply@anthropic.com
should NOT be flagged, and the amend remedy should clear the warning.
Error Messages/Logs
# ── Claude Code on the web — cloud session ──────────────────────────────────
# Repo: kleer001/book_loom Branch: claude/book-loom-critique-rewrite-hbv7l4
# 1) Signing IS configured and active (SSH signing, web-provisioned):
$ git config --get-regexp '^(commit\.gpgsign|gpg\.|user\.signingkey)'
user.signingkey /home/claude/.ssh/commit_signing_key.pub
gpg.format ssh
gpg.ssh.program /tmp/code-sign
commit.gpgsign true
# 2) The signing PUBLIC KEY file is EMPTY (0 bytes), and the signer is sign-only:
$ ls -la "$(git config user.signingkey)"
-rw-r--r-- 1 claude claude 0 Mar 31 13:31 /home/claude/.ssh/commit_signing_key.pub
$ ls -la /tmp/code-sign
lrwxrwxrwx 1 root root 35 /tmp/code-sign -> /opt/env-runner/environment-manager
# 3) allowedSignersFile is UNSET, so local verification is impossible:
$ git config gpg.ssh.allowedSignersFile
$ echo $?
1
# 4) The commits ARE genuinely signed — note the gpgsig SSH header & committer:
$ git cat-file commit HEAD | head -6
tree dcfa8a69ce4546c4934b8db5a2020068c446bd0b
parent 814891bb69130e05fd94d7626b0dad7aebffa232
author Claude <noreply@anthropic.com> 1781716009 +0000
committer Claude <noreply@anthropic.com> 1781716009 +0000
gpgsig -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg...
# 5) But because verification can't run, git reports %G? = N (== "no signature"),
# identical to an unsigned commit. THIS is what the hook keys on:
$ git log --format='%h %G? %ce %s' -3
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
7a929d1 N noreply@anthropic.com Refine reloom spec: input formats, ingest, and the Mandate workflow
814891b N noreply@anthropic.com Add spec for inverting book_loom into a critique-and-rewrite pipeline
$ git verify-commit HEAD
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
$ git show --show-signature -s HEAD | head -3
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
commit 7a929d1602a79451a197e11072a599365664a185
No signature # ← signed commit reported as "No signature"
# 6) The Stop hook (~/.claude/stop-hook-git-check.sh, line ~54) does:
# git log --format='%h %G? %ce' "$upstream..HEAD" | awk '$2=="N" || $3!="noreply@anthropic.com"'
# and emits (exit 2), every single turn:
[~/.claude/stop-hook-git-check.sh]: There are commit(s) on branch
'claude/book-loom-critique-rewrite-hbv7l4' that GitHub will show as Unverified
(missing signature, or committer email is not noreply@anthropic.com):
7a929d1 N noreply@anthropic.com
814891b N noreply@anthropic.com
Please run 'git config user.email noreply@anthropic.com && git config user.name Claude',
then 'git commit --amend --no-edit --reset-author' ...
# 7) The prescribed remedy does NOT help: amend re-signs the same way, %G? is still N.
# The check is unsatisfiable for unpushed commits → fires on every turn (token burn).
# 8) The ONLY thing that silences it: push, so the scanned range empties.
$ git rev-list "origin/$(git branch --show-current)..HEAD" --count
0 # after pushing, upstream..HEAD is empty → hook finally exits 0
Steps to Reproduce
Steps to reproduce
- In a web session, make a commit (it is auto-signed; committer = noreply@anthropic.com).
- End the turn → Stop hook reports the commit as Unverified.
- Run the prescribed
git commit --amend --no-edit --reset-author(or therebase --execvariant). - End the turn again → the same commit is reported again. Loop repeats indefinitely.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.170
Claude Code Version
2.1.179
Platform
Anthropic API
Operating System
Other
Terminal/Shell
Other
Additional Information
Actual
Signed commits are flagged identically to unsigned; the amend remedy is a no-op; the hook loops every turn until the branch is pushed.
Suggested fixes (any one)
- Detect signature presence without
%G?— e.g. check for agpgsigheader (git cat-file commit <sha>/git log --format=%GK). - Provision
gpg.ssh.allowedSignersFile(and a non-empty pubkey) so%G?can actually verify. - When local verification is known-unavailable in the runtime, skip the signature branch and rely on the committer-email + unpushed checks only.
Workaround
Push the branch — origin/<branch>..HEAD empties and the hook goes silent; no --amend needed. Commits show Verified on GitHub regardless.
Related
- #7711 (commit-signing feature request, closed not planned — but the web runtime does sign now; the verification check is what's broken)
- Same loop class as #55754 / #58348 / #3573 / #10205, distinct root cause (
%G?=Non validly SSH-signed commits with no local verification).
---
Disclosure: This report — including the reproduction log and root-cause analysis — was drafted by Claude Code (Opus 4.8) running in the affected web session, at the reporter's direction. Commands and outputs in the log were executed live in that session.