Skip to content

Instantly share code, notes, and snippets.

@jadb
Last active September 14, 2026 15:58
Show Gist options
  • Select an option

  • Save jadb/9848a18a3534ee52700e90507d512ed8 to your computer and use it in GitHub Desktop.

Select an option

Save jadb/9848a18a3534ee52700e90507d512ed8 to your computer and use it in GitHub Desktop.
Block AI/co-author attribution from reaching git remotes and GitHub: a pre-push hook plus a gh wrapper

Keep AI attribution out of git and GitHub

Two guards that stop Co-Authored-By: trailers, πŸ€– Generated with … footers and session URLs from reaching a remote.

They catch different things, and you want both β€” a pre-push hook never sees a PR body, and a gh wrapper never sees a commit message.

Guard Covers
pre-push commit messages, author and committer fields
gh --body / -b / --body-file / -F / --notes / --notes-file on pr, issue, release, gist, api

Prose mentioning a tool passes; attribution metadata does not:

feat(claude-api): add streaming support        allowed
docs: update Claude model IDs in the README    allowed
fix: handle co-authored PRs from squash merge  allowed

Co-Authored-By: Claude <noreply@anthropic.com>   BLOCKED
πŸ€– Generated with [Claude Code](...)             BLOCKED
https://claude.ai/code/session_01ABC             BLOCKED

Install

# 1. pre-push hook, applied to every repo
mkdir -p ~/.config/git/hooks
install -m 755 pre-push ~/.config/git/hooks/pre-push
git config --global core.hooksPath ~/.config/git/hooks

# 2. gh wrapper β€” must sit ahead of the real gh on PATH
install -m 755 gh ~/.local/bin/gh

Verify the wrapper actually intercepts:

which gh          # => ~/.local/bin/gh, not /opt/homebrew/bin/gh

If it still resolves to the real binary, something later in your shell startup is re-prepending that directory. Homebrew's brew shellenv is the usual culprit β€” it prepends /opt/homebrew/bin, so anything that adds ~/.local/bin before that line loses. Add this after it:

path=("$HOME/.local/bin" ${path:#$HOME/.local/bin})

Escape hatch

Both honour ALLOW_ATTRIBUTION=1. You need it when GitHub's squash-merge adds a legitimate human Co-authored-by: trailer.

Caveats

  • A repo-local core.hooksPath (Husky, .githooks/) shadows the global hook. Check with git config --get core.hooksPath. To cover such a repo, chain the check from its own hook β€” but note that if the hooks directory is tracked in git, you are committing your personal policy into a shared repo.
  • The wrapper only covers the gh CLI. The web UI and other API clients are unguarded.
  • pre-push scans only commits in the push range, so it is cheap on large repos, but it will not catch attribution already on the remote.
#!/bin/bash
# gh wrapper: blocks PR/issue/release/gist bodies that carry AI or
# co-author attribution, before they reach GitHub.
set -uo pipefail
real_gh=""
self=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/$(basename "${BASH_SOURCE[0]}")
while IFS= read -r cand; do
[ -x "$cand" ] || continue
c=$(cd "$(dirname "$cand")" && pwd -P)/$(basename "$cand")
[ "$c" = "$self" ] && continue
real_gh="$cand"; break
done < <(which -a gh 2>/dev/null)
if [ -z "$real_gh" ]; then
for c in /opt/homebrew/bin/gh /usr/local/bin/gh /usr/bin/gh; do
[ -x "$c" ] && { real_gh="$c"; break; }
done
fi
if [ -z "$real_gh" ]; then
echo "gh wrapper: cannot find the real gh binary" >&2
exit 127
fi
if [ "${ALLOW_ATTRIBUTION:-0}" = "1" ]; then
exec "$real_gh" "$@"
fi
PATTERN='co-authored-by|πŸ€–|generated with \[?claude|claude\.ai/code|claude-session:|generated by (claude|chatgpt|copilot|gpt)|with help from (claude|chatgpt|copilot)'
# Only inspect body-bearing subcommands.
case " $* " in
*" pr "*|*" issue "*|*" release "*|*" gist "*|*" api "*) ;;
*) exec "$real_gh" "$@" ;;
esac
violation=""
prev=""
for arg in "$@"; do
case "$prev" in
--body|-b|--notes|--body-file|-F|--notes-file)
text="$arg"
case "$prev" in
--body-file|-F|--notes-file)
if [ "$arg" = "-" ]; then text=""; else text=$(cat -- "$arg" 2>/dev/null || true); fi
;;
esac
if printf '%s' "$text" | grep -qiE "$PATTERN"; then
violation=$(printf '%s' "$text" | grep -inE "$PATTERN" | head -5)
fi
;;
esac
prev="$arg"
done
if [ -n "$violation" ]; then
{
echo ""
echo "gh BLOCKED β€” attribution found in the body text:"
echo ""
printf '%s\n' "$violation" | sed 's/^/ /'
echo ""
echo "Remove it before publishing."
echo ""
} >&2
exit 1
fi
exec "$real_gh" "$@"
#!/bin/bash
# Block pushes containing AI/co-author attribution.
# Rejects any push whose commit messages or author/committer fields carry
# AI or co-author attribution.
set -uo pipefail
[ "${ALLOW_ATTRIBUTION:-0}" = "1" ] && exit 0
PATTERN='co-authored-by|πŸ€–|generated with \[?claude|claude\.ai/code|claude-session:|generated by (claude|chatgpt|copilot|gpt)|with help from (claude|chatgpt|copilot)'
Z40=0000000000000000000000000000000000000000
fail=0
while read -r _local_ref local_sha _remote_ref remote_sha; do
# Deleting a ref β€” nothing to inspect.
[ "$local_sha" = "$Z40" ] && continue
# Only commits not already reachable from ANY remote ref: merging main into a
# branch must not re-scan main's own commits (GitHub squash merges carry a
# human Co-authored-by trailer that would trip the pattern).
range=$(git rev-list "$local_sha" --not --remotes)
[ -z "$range" ] && continue
for sha in $range; do
msg=$(git log -1 --format='%B%n%an <%ae>%n%cn <%ce>' "$sha")
if printf '%s' "$msg" | grep -qiE "$PATTERN"; then
if [ "$fail" -eq 0 ]; then
echo "" >&2
echo "PUSH BLOCKED β€” attribution found in commit metadata:" >&2
echo "" >&2
fi
echo " $(git log -1 --format='%h %s' "$sha")" >&2
printf '%s' "$msg" | grep -inE "$PATTERN" | sed 's/^/ /' >&2
fail=1
fi
done
done
if [ "$fail" -ne 0 ]; then
cat >&2 <<'MSG'
Remove the attribution before pushing. To rewrite the offending messages:
git rebase -i --root # or: git commit --amend (last commit only)
MSG
exit 1
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment