From 8fef2f7554a099d5503f5b0fcf035b7edee372f5 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 28 Sep 2026 18:10:16 +0000 Subject: [PATCH] Check commit author/committer identity for AI attribution, not just message check_no_ai_attribution.sh only ever scanned commit message text. A recent PR carried a commit with a clean message but an AI-tool author identity, so it went undetected. Give the script an --identity mode for checking a "Name " string, and wire it into commit-msg (via `git var GIT_AUTHOR_IDENT`/`GIT_COMMITTER_IDENT`, available before the commit exists) and into pre-push (scanning each pushed commit's author AND committer, since a bad identity can slip in via either field). --- ci_scripts/check_no_ai_attribution.sh | 34 +++++++++++++++++++++++---- ci_scripts/git-hooks/commit-msg | 9 ++++++- ci_scripts/git-hooks/pre-push | 12 ++++++++-- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/ci_scripts/check_no_ai_attribution.sh b/ci_scripts/check_no_ai_attribution.sh index c7b4d2a725f..cb6f5581bc6 100755 --- a/ci_scripts/check_no_ai_attribution.sh +++ b/ci_scripts/check_no_ai_attribution.sh @@ -14,18 +14,35 @@ # Usage: # check_no_ai_attribution.sh # commit-msg hook use # git log --format=%B -1 | check_no_ai_attribution.sh - # pre-push hook use +# check_no_ai_attribution.sh --identity # author/committer identity check # # Deliberately checks the commit message only, not file/diff content: a patch # can legitimately carry a real upstream contributor's byline (a person can be # named Claude), and this check has no way to tell that apart from an AI # attribution trailer. The message text is ours to write, so it has no such # ambiguity. +# +# --identity is different: it checks an author/committer "Name " string +# (e.g. `git var GIT_AUTHOR_IDENT`, or `git log --format='%an <%ae>'`), not a +# message. There's no "real person happens to be named Claude" ambiguity to +# worry about here - this repo's commit identity is always +# `Ludovic Henry `, so any identity matching the pattern is +# an AI-tool identity, full stop (PR #2448 leaked a `Claude ` +# author this way, past the message-only check above). set -eu pattern='claude|anthropic' -file="${1:?usage: check_no_ai_attribution.sh }" +mode="${1:?usage: check_no_ai_attribution.sh | --identity }" +if [ "$mode" = "--identity" ]; then + field="identity" + file="${2:?usage: check_no_ai_attribution.sh --identity }" +else + field="message" + file="$mode" +fi + if [ "$file" = "-" ]; then content="$(cat)" else @@ -34,10 +51,17 @@ fi hit="$(printf '%s\n' "$content" | grep -inE "$pattern" || true)" if [ -n "$hit" ]; then - echo "check_no_ai_attribution: commit message mentions Claude/Anthropic - this project never attributes work to an AI tool." >&2 - echo " Offending line(s):" >&2 - echo "$hit" | sed 's/^/ /' >&2 - echo " Remove any Co-Authored-By/Claude-Session/\"Generated by\" trailer and rewrite the message, then retry." >&2 + if [ "$field" = "identity" ]; then + echo "check_no_ai_attribution: commit author/committer identity mentions Claude/Anthropic - this project never commits under an AI-tool identity." >&2 + echo " Offending line(s):" >&2 + echo "$hit" | sed 's/^/ /' >&2 + echo " Set the commit identity to Ludovic Henry and retry." >&2 + else + echo "check_no_ai_attribution: commit message mentions Claude/Anthropic - this project never attributes work to an AI tool." >&2 + echo " Offending line(s):" >&2 + echo "$hit" | sed 's/^/ /' >&2 + echo " Remove any Co-Authored-By/Claude-Session/\"Generated by\" trailer and rewrite the message, then retry." >&2 + fi exit 1 fi diff --git a/ci_scripts/git-hooks/commit-msg b/ci_scripts/git-hooks/commit-msg index b731f9a3c15..73fa45de7ee 100755 --- a/ci_scripts/git-hooks/commit-msg +++ b/ci_scripts/git-hooks/commit-msg @@ -10,4 +10,11 @@ # shared core.hooksPath) runs fine. common_dir="$(git rev-parse --path-format=absolute --git-common-dir)" main_toplevel="$(dirname "$common_dir")" -exec "$main_toplevel/ci_scripts/check_no_ai_attribution.sh" "$1" +checker="$main_toplevel/ci_scripts/check_no_ai_attribution.sh" + +"$checker" "$1" || exit 1 + +# Also reject the identity the commit is about to be made under - available +# even before the commit exists via `git var`. +git var GIT_AUTHOR_IDENT | "$checker" --identity - || exit 1 +git var GIT_COMMITTER_IDENT | "$checker" --identity - || exit 1 diff --git a/ci_scripts/git-hooks/pre-push b/ci_scripts/git-hooks/pre-push index 2a392dc626c..276e8136da0 100755 --- a/ci_scripts/git-hooks/pre-push +++ b/ci_scripts/git-hooks/pre-push @@ -7,7 +7,7 @@ # catches a commit made before the hook was installed, an amend/rebase that # reintroduced one, or a merge commit whose own message was never run through # commit-msg. Scans every commit about to be pushed that the remote doesn't -# already have. +# already have - both its message and its author/committer identity. set -eu @@ -45,7 +45,15 @@ while read -r local_ref local_sha remote_ref remote_sha; do if ! printf '%s' "$msg" | "$checker" - 2>/tmp/check_no_ai_attribution.$$; then echo "check_no_ai_attribution: blocked in commit $(git rev-parse --short "$commit") ($local_ref):" >&2 cat /tmp/check_no_ai_attribution.$$ >&2 - rm -f /tmp/check_no_ai_attribution.$$ + status=1 + fi + rm -f /tmp/check_no_ai_attribution.$$ + + # Author AND committer: a bad identity can slip in via either field. + idents="$(git log --format='%an <%ae>|%cn <%ce>' -1 "$commit" | tr '|' '\n')" + if ! printf '%s\n' "$idents" | "$checker" --identity - 2>/tmp/check_no_ai_attribution.$$; then + echo "check_no_ai_attribution: blocked in commit $(git rev-parse --short "$commit") ($local_ref):" >&2 + cat /tmp/check_no_ai_attribution.$$ >&2 status=1 fi rm -f /tmp/check_no_ai_attribution.$$