the static git browser that builds this site
git clone https://git.lucas.co/gitsite.git
Hooks: scan staged changes for secrets before the push hook publishes them
pre-commit-scan runs gitleaks on the staged diff and refuses a commit that
looks like it carries a credential -- needed here specifically because
post-commit-push puts every commit on GitHub seconds later, leaving no
window to notice. Where gitleaks is not installed the commit goes through
with a warning, so a fresh machine can still commit. install-hooks.sh now
symlinks both hooks.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
install-hooks.sh | 29 +++++++++++++++++++----------
pre-commit-scan | 37 +++++++++++++++++++++++++++++++++++++
readme.txt | 13 ++++++++++++-
3 files changed, 68 insertions(+), 11 deletions(-)
diff --git a/install-hooks.sh b/install-hooks.sh
index 45f4d8d..7aea017 100755
--- a/install-hooks.sh
+++ b/install-hooks.sh
@@ -1,7 +1,8 @@
#!/bin/sh
-# Symlink post-commit-push into every repo listed in repos.conf, as its
-# post-commit hook. Safe to re-run: an existing symlink is refreshed, and a
-# hook that is a real file (someone's own) is left alone and reported.
+# Symlink the hooks into every repo listed in repos.conf: post-commit-push as
+# its post-commit hook and pre-commit-scan as its pre-commit hook. Safe to
+# re-run: an existing symlink is refreshed, and a hook that is a real file
+# (someone's own) is left alone and reported.
#
# Work trees are found by NAME under $GIT_WORK_ROOTS, since repos.conf records
# only where a repo is mirrored from. A name that resolves to nothing fails
@@ -9,7 +10,8 @@
set -eu
BASE=$(dirname "$(readlink -f "$0")")
-HOOK="$BASE/post-commit-push"
+# hook-name=file, one pair per word.
+HOOKS="pre-commit=pre-commit-scan post-commit=post-commit-push"
WORK_ROOTS="${GIT_WORK_ROOTS:-$HOME/projects/cce $HOME/projects}"
FAILED=""
@@ -25,14 +27,21 @@ for name in $names; do
continue
fi
hooks=$(git -C "$wt" rev-parse --path-format=absolute --git-path hooks)
- dest="$hooks/post-commit"
- if [ -e "$dest" ] && [ ! -L "$dest" ]; then
- echo "!! $name: $dest exists and is not a symlink -- left alone"
+ mkdir -p "$hooks"
+ bad=""
+ for pair in $HOOKS; do
+ dest="$hooks/${pair%%=*}"
+ if [ -e "$dest" ] && [ ! -L "$dest" ]; then
+ echo "!! $name: $dest exists and is not a symlink -- left alone"
+ bad=1
+ continue
+ fi
+ ln -sfn "$BASE/${pair#*=}" "$dest"
+ done
+ if [ -n "$bad" ]; then
FAILED="$FAILED $name"
continue
fi
- mkdir -p "$hooks"
- ln -sfn "$HOOK" "$dest"
echo " $name"
done
@@ -41,4 +50,4 @@ if [ -n "$FAILED" ]; then
echo "done, with failures:$FAILED"
exit 1
fi
-echo "done: post-commit hook installed in every repo"
+echo "done: pre-commit and post-commit hooks installed in every repo"
diff --git a/pre-commit-scan b/pre-commit-scan
new file mode 100755
index 0000000..7c7bfd3
--- /dev/null
+++ b/pre-commit-scan
@@ -0,0 +1,37 @@
+#!/bin/sh
+# pre-commit hook: refuse a commit whose staged changes look like a secret.
+#
+# Installed by install-hooks.sh next to post-commit-push, and it exists
+# because of that hook: a commit here is on GitHub seconds later, so there is
+# no window to notice a pasted credential before it is public. Only the
+# staged diff is scanned, so it costs well under a second.
+#
+# Uses gitleaks when it is on PATH. Without it the commit goes through, with a
+# warning on stderr -- a fresh machine must still be able to commit -- so
+# install it to get the protection: `pacman -S gitleaks` on Arch, `brew
+# install gitleaks` on macOS, or a binary from
+# https://github.com/gitleaks/gitleaks/releases.
+#
+# A finding blocks the commit and prints where it is, value redacted. For a
+# true false positive, allowlist it in the repo's .gitleaks.toml or pass
+# --no-verify to that one commit -- remembering that the push hook publishes
+# whatever gets past this one.
+
+if ! command -v gitleaks >/dev/null 2>&1; then
+ echo "pre-commit: gitleaks is not installed -- staged changes were NOT scanned for secrets" >&2
+ exit 0
+fi
+
+# `gitleaks git --pre-commit` is the 8.19+ spelling; older releases have
+# `protect` instead. Both scan only what is staged.
+if gitleaks git --help >/dev/null 2>&1; then
+ gitleaks git --pre-commit --staged --redact --verbose --no-banner
+else
+ gitleaks protect --staged --redact --verbose --no-banner
+fi
+status=$?
+if [ "$status" -ne 0 ]; then
+ echo "pre-commit: the staged changes look like they contain a secret -- commit refused." >&2
+ echo " false positive? allowlist it in .gitleaks.toml, or git commit --no-verify" >&2
+fi
+exit "$status"
diff --git a/readme.txt b/readme.txt
index b029ced..fc21562 100644
--- a/readme.txt
+++ b/readme.txt
@@ -23,8 +23,13 @@ gitsite.service, gitsite.timer
post-commit-push
git post-commit hook: pushes the committed branch to origin,
so committing is publishing -- see "Publishing model" below
+pre-commit-scan
+ git pre-commit hook: refuses a commit whose staged changes
+ look like a secret (gitleaks), since the push hook would
+ publish it seconds later; warns and lets the commit through
+ where gitleaks is not installed
install-hooks.sh
- symlinks post-commit-push into every repo in repos.conf
+ symlinks both hooks into every repo in repos.conf
style.css matches lucas.co (black, white, blue links, Circe)
Workflow
@@ -71,6 +76,12 @@ IS the publishing step. The hook never forces: a rejected push leaves the
commit local and says so, and you resolve it with git pull / git push as
usual. It does nothing on a detached HEAD or mid-rebase.
+Because of that, pre-commit-scan runs first: with gitleaks installed it
+scans the staged diff and refuses a commit that looks like it carries a
+credential, so nothing of the kind reaches GitHub. Without gitleaks the
+commit goes through with a warning -- install it (pacman -S gitleaks) to
+have the check actually run.
+
The 27 crates pinned to https://git.lucas.co/<name>.git?rev=... keep
working because the dumb-http clone dirs are built from the same mirrors;
a rev exists here as long as it is reachable on GitHub.