Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions ci_scripts/check_no_ai_attribution.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,35 @@
# Usage:
# check_no_ai_attribution.sh <file-with-commit-message> # commit-msg hook use
# git log --format=%B -1 <sha> | check_no_ai_attribution.sh - # pre-push hook use
# check_no_ai_attribution.sh --identity <file|-> # 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 <email>" 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 <git@ludovic.dev>`, so any identity matching the pattern is
# an AI-tool identity, full stop (PR #2448 leaked a `Claude <noreply@anthropic.com>`
# author this way, past the message-only check above).

set -eu

pattern='claude|anthropic'

file="${1:?usage: check_no_ai_attribution.sh <file|->}"
mode="${1:?usage: check_no_ai_attribution.sh <file|-> | --identity <file|->}"
if [ "$mode" = "--identity" ]; then
field="identity"
file="${2:?usage: check_no_ai_attribution.sh --identity <file|->}"
else
field="message"
file="$mode"
fi

if [ "$file" = "-" ]; then
content="$(cat)"
else
Expand All @@ -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 <git@ludovic.dev> 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

Expand Down
9 changes: 8 additions & 1 deletion ci_scripts/git-hooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 10 additions & 2 deletions ci_scripts/git-hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.$$
Expand Down
Loading