[BUG] stop-hook-git-check.sh false-positives on SSH-signed commits (%G?=N), creating an unbreakable Stop-hook loop

Open 💬 0 comments Opened Jun 17, 2026 by kleer001

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.allowedSignersFile is not configured, and
  • the public key file ~/.ssh/commit_signing_key.pub is empty (0 bytes), and
  • the signer /tmp/code-sign is sign-only (error: ... only SSH-style signing (-Y sign) is supported).

So git can't verify, prints
error: gpg.ssh.allowedSignersFile needs to be configured and exist, and
reports N. The commits are genuinely signed — git cat-file commit HEAD shows a
gpgsig -----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)

  1. 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.

  1. Provision gpg.ssh.allowedSignersFile from the signing public key so %G?

can actually verify — but note the key file is currently empty, so that needs
fixing too.

  1. 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

  1. In a web session, make a commit (it is auto-signed; committer = noreply@anthropic.com).
  2. End the turn → Stop hook reports the commit as Unverified.
  3. Run the prescribed git commit --amend --no-edit --reset-author (or the rebase --exec variant).
  4. 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)

  1. Detect signature presence without %G? — e.g. check for a gpgsig header (git cat-file commit <sha> / git log --format=%GK).
  2. Provision gpg.ssh.allowedSignersFile (and a non-empty pubkey) so %G? can actually verify.
  3. 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?=N on 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.

View original on GitHub ↗