From 35f024c159836d000960f38fc1a3ce86f901d1ff Mon Sep 17 00:00:00 2001 From: Rootiest Date: Sat, 12 Sep 2026 20:49:33 -0400 Subject: [PATCH] feat(git): add pre-push hook to reject unsigned commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gittyup commits via libgit2 directly and never invokes gpg, silently ignoring commit.gpgsign — root cause of an unsigned commit reaching main. Adds a tracked .githooks/pre-push that rejects any push carrying a commit with no signature or a bad signature, bypassable with --no-verify. Wiring core.hooksPath to it is a per-machine concern, done separately in user-dots, not shipped here. --- .githooks/pre-push | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..35fdd4e --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Rejects a push that would put an unsigned or bad-signature commit onto a +# branch. Root cause this guards against: GUI git clients (e.g. Gittyup) +# commit via libgit2 directly and never invoke gpg, silently ignoring +# commit.gpgsign. Bypass intentionally with `git push --no-verify`. +zero=0000000000000000000000000000000000000000 + +while read -r local_ref local_sha remote_ref _remote_sha; do + [ "$local_sha" = "$zero" ] && continue # branch deletion + case "$remote_ref" in refs/heads/*) ;; *) continue ;; esac + + if [ "$_remote_sha" = "$zero" ]; then + range="$local_sha --not --remotes" + else + range="$_remote_sha..$local_sha" + fi + + bad="" + for sha in $(git rev-list $range --); do + sig="$(git log -1 --pretty=%G? "$sha")" + case "$sig" in + N | B) bad="$bad $sha" ;; + esac + done + + if [ -n "$bad" ]; then + echo "pre-push: unsigned or bad-signature commit(s) pushing to $remote_ref, refusing:" >&2 + for sha in $bad; do + git log -1 --pretty=' %h %G? %s' "$sha" >&2 + done + echo "Fix: git commit --amend -S (or rebase -i + amend), then push again." >&2 + echo "Bypass: git push --no-verify" >&2 + exit 1 + fi +done + +# Chain to the global/system hook this local override is shadowing. +global_hooks="$(git config --global core.hooksPath 2>/dev/null)" +[ -z "$global_hooks" ] && global_hooks="$(git config --system core.hooksPath 2>/dev/null)" +if [ -n "$global_hooks" ]; then + global_hooks="${global_hooks/#\~/$HOME}" # git stores ~ verbatim + [ -x "$global_hooks/pre-push" ] && exec "$global_hooks/pre-push" "$@" +fi +exit 0