1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
#!/usr/bin/env dash
# Reject pushes that include commits which:
# * lack a good signature, or
# * have an author, committer, or coauthor that looks like a coding
# agent (Copilot, Claude, Codex, ChatGPT, Cursor, Aider, Devin, ...).
# I want human names on anything I push, even when an agent helped
# write it. Use `git commit --amend --reset-author` after
# agent-authored work.
#
# Activated via core.hooksPath in ~/.config/git/config so it applies to
# every repo unless that repo overrides hooksPath itself (this dotfiles
# repo does, pointing at .githooks/ which delegates here).
#
# Bypass for one push: git push --no-verify
set -eu
# shellcheck source=./_dispatch.sh
. "${0%/*}/_dispatch.sh"
# Buffer stdin so both the per-repo hook and our own loop below get the
# full ref list (git only feeds it once).
_stdin_buf=$(mktemp)
trap 'rm -f "$_stdin_buf"' EXIT INT TERM
cat >"$_stdin_buf"
dispatch_repo_hook pre-push "$@" <"$_stdin_buf"
zero=$(git hash-object --stdin </dev/null | tr '0-9a-f' '0')
# %G? signature status codes from git-log:
# G good signature
# U good, unknown validity (e.g. trust level not set)
# X good but expired signature
# Y good but key is expiring
# R good but key has been revoked
# B bad signature
# E signature cannot be checked (e.g. missing key)
# N no signature
# We accept G/U/X/Y and reject anything else.
ok='G U X Y'
# Case-insensitive substrings that disqualify an identity. Matched against
# "<name> <email>" lowercased. Plain substring matching (index()) is
# used in the awk below to dodge regex-escaping pitfalls. Keep this list
# narrow on purpose: false positives are worse than misses (a slipped
# commit can be amended and force-pushed; a noisy hook gets disabled).
agent_subs='copilot claude codex chatgpt cursor aider devin [bot] @openai. @anthropic.'
fail=0
while read -r _local_ref local_sha remote_ref remote_sha; do
# Branch deletion: nothing to verify on our side.
[ "$local_sha" = "$zero" ] && continue
if [ "$remote_sha" = "$zero" ]; then
# New branch: verify the commits unique to this push, excluding
# anything already reachable from any remote-tracking ref.
set -- "$local_sha" --not --remotes
else
set -- "${remote_sha}..${local_sha}"
fi
# One pass: per-commit emit
# SHA \t %G? \t cn \t ce \t an \t ae \t co-authored-by-list \t subject
# Co-authored-by trailers are joined with 0x1f (unit separator) which
# cannot appear in identity strings. Tabs aren't valid in identity
# fields either, so awk -F'\t' parses unambiguously.
bad=$(git rev-list \
--format='%H%x09%G?%x09%cn%x09%ce%x09%an%x09%ae%x09%(trailers:key=Co-authored-by,valueonly,unfold,separator=%x1f)%x09%s' \
--no-commit-header "$@" |
awk -F'\t' \
-v ok="$ok" \
-v agent_subs="$agent_subs" '
BEGIN {
split(ok, a, " "); for (i in a) good[a[i]] = 1
n_subs = split(agent_subs, subs, " ")
}
function is_agent(s, i) {
for (i = 1; i <= n_subs; i++) if (index(s, subs[i])) return 1
return 0
}
{
reasons = ""
if (!($2 in good)) reasons = reasons " [sig=" $2 "]"
if (is_agent(tolower($3 " " $4))) {
reasons = reasons " [agent-committer=" $3 " <" $4 ">]"
}
if (is_agent(tolower($5 " " $6))) {
reasons = reasons " [agent-author=" $5 " <" $6 ">]"
}
if ($7 != "") {
n_co = split($7, coauthors, "\037")
for (i = 1; i <= n_co; i++) {
if (coauthors[i] != "" && is_agent(tolower(coauthors[i]))) {
reasons = reasons " [agent-coauthor=" coauthors[i] "]"
}
}
}
if (reasons != "") print $1 reasons " " $8
}
')
if [ -n "$bad" ]; then
if [ "$fail" -eq 0 ]; then
printf '\nrefusing to push: bad commits found\n' >&2
fi
printf '\non %s:\n%s\n' "$remote_ref" "$bad" >&2
fail=1
fi
done <"$_stdin_buf"
if [ "$fail" -ne 0 ]; then
printf '\nfix signature:\n' >&2
printf ' git rebase --exec "git commit --amend --no-edit -S" <base>\n' >&2
printf 'fix AI identity (re-stamp yourself as author/committer, keep agent out):\n' >&2
printf ' git rebase --exec "git commit --amend --no-edit --reset-author -S" <base>\n' >&2
printf 'bypass:\n git push --no-verify\n\n' >&2
exit 1
fi
exit 0
|