From 992ff8256e0711428ec152148a7a14ab5beef4fe Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 9 Aug 2026 12:46:55 +0200 Subject: [PATCH] ci: gate pushes on a gitleaks scan of the outgoing commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No repo here scanned for committed credentials. The hook is self-contained rather than delegating to a Makefile, because this repo has none and a hook reaching into a sibling repo breaks the moment this one is cloned elsewhere. Scans the outgoing range rather than full history: history carries settled findings — test fixtures, vendored third-party code — and a gate that fails on something unfixable gets bypassed within a week. Setting core.hooksPath means pql init must replant its replication shims into .githooks, which is why they are gitignored here alongside the tracked pre-push. Same layout pql itself uses. Co-Authored-By: Claude --- .githooks/pre-push | 52 ++++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 8 +++++++ 2 files changed, 60 insertions(+) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..5a8e702 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Secret scan over the commits about to be pushed. Opt in per clone with: +# +# git config core.hooksPath .githooks +# +# Self-contained on purpose: this repo has no Makefile, and a hook that +# depends on one in a sibling repo breaks the moment the repo is cloned +# anywhere else. +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# A non-login shell — which is what git gives a hook — skips /etc/profile.d +# and never sees ~/.local/bin, where the gitleaks release tarball lands. +# Without this the scan reports "not installed" on every push. +[ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH" + +if ! command -v gitleaks >/dev/null 2>&1; then + echo "FAIL secrets — gitleaks not installed, so this check would be a no-op pretending to pass." >&2 + echo " https://github.com/gitleaks/gitleaks/releases → ~/.local/bin/gitleaks" >&2 + exit 1 +fi + +# Scan the outgoing range, not full history. History here carries findings +# that are settled — test fixtures and vendored third-party code — and a gate +# that fails on something unfixable gets bypassed within a week. What matters +# is what is about to leave this machine. +if upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null); then + range="$upstream..HEAD" +elif git rev-parse --verify --quiet origin/main >/dev/null; then + range="origin/main..HEAD" +else + range="" +fi + +if [ -z "$range" ]; then + gitleaks dir . --redact --no-banner --exit-code 1 || { + echo "FAIL secrets — gitleaks found a credential in the working tree." >&2; exit 1; } + exit 0 +fi + +[ -n "$(git log --oneline "$range" 2>/dev/null)" ] || exit 0 + +gitleaks git . --log-opts="$range" --redact --no-banner --exit-code 1 >/dev/null 2>&1 || { + echo "FAIL secrets — gitleaks found a credential in the commits being pushed." >&2 + echo " inspect (values redacted): gitleaks git . --log-opts=\"$range\" --redact" >&2 + echo " then remove and rotate it, or suppress deliberately:" >&2 + echo " inline '# gitleaks:allow '" >&2 + echo " or add the fingerprint to .gitleaksignore WITH a reason" >&2 + exit 1 +} +echo " ok secrets" diff --git a/.gitignore b/.gitignore index 5b16f66..e365a1f 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,11 @@ webber-sandbox/.current_template .claude/settings.local.json .pql/* !.pql/changelog/ + +# pql shims planted by `pql init` into the dir core.hooksPath points at. +# Per-clone: each embeds the absolute path of the pql binary that planted it. +# Only .githooks/pre-push is shared. +.githooks/pre-commit +.githooks/post-merge +.githooks/post-checkout +.githooks/post-rewrite