#!/bin/sh # hooks/commit-msg -- keep commit trailers in this project's attribution form. # # card-games is published (MELPA, and bound for GNU ELPA), so every commit # message is a permanent public record. This hook refuses two trailers the # project has not agreed to and checks the one form it has: # # refused a session-link trailer (Claude-Session and the like) # refused Co-Authored-By naming Claude or anthropic.com # checked Assisted-by: NAME (for PERSON; MODEL) # e.g. Assisted-by: Mentat (for Corwin; claude-opus-5-5) # A comma in place of the semicolon is accepted too. # # Attribution is the author's call: a message with no Assisted-by line # passes. Comment lines, and everything after the scissors line that # `git commit -v' adds, are not checked. # # Install: make hooks (also installs applypatch-msg, so `git am' is checked) # Bypass: git commit --no-verify -- a hook is a check, not a lock. # Tests: make test-hooks # # Usage: commit-msg MESSAGE-FILE Exit 0 accepts, 1 refuses. set -u msg=${1:?usage: commit-msg MESSAGE-FILE} cr=$(printf '\r') status=0 refuse () { printf 'commit-msg: %s\n %s\n' "$1" "$2" >&2 status=1 } while IFS= read -r line || [ -n "$line" ]; do line=${line%"$cr"} case $line in '# ------------------------ >8 ------------------------'*) break ;; '#'*) continue ;; esac lower=$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]') case $lower in claude-session:*) refuse 'session links are not used in this project:' "$line" ;; co-authored-by:*claude*|co-authored-by:*anthropic.com*) refuse 'credit AI help as Assisted-by: NAME (for PERSON; MODEL), not:' "$line" ;; assisted-by:*) if ! printf '%s\n' "$line" | grep -Eq '^[Aa]ssisted-[Bb]y: [^ (][^(]* \(for [^;,()]+[;,] [^ ;,()]+\)$' then refuse 'expected Assisted-by: NAME (for PERSON; MODEL), got:' "$line" fi ;; esac done < "$msg" if [ "$status" -ne 0 ]; then echo 'commit-msg: refused; fix the message and try again.' >&2 fi exit "$status"