githooks: one set of git checks for every repo, without breaking theirs¶
The problem¶
Git's core.hooksPath setting points every repository at one hooks folder,
which is the obvious way to run the same checks everywhere: no secrets, no
stray personal files, nothing huge. But it replaces each repo's own
.git/hooks rather than adding to it. The project's lint hook stops running.
Git LFS, which uploads large files from a pre-push hook, stops uploading.
Nobody notices until a push is rejected or a file is missing.
The method¶
Every hook name in the global folder links to one dispatcher. Called as
pre-commit, pre-push and so on, it runs each executable in <hook>.d/
(your checks, as drop-in files), then the repo's own hook, then git lfs <hook>
for LFS repos that have no hook of their own.
Three details decide whether it works:
- Find the repo's own hook with
git rev-parse --git-common-dir.--git-path hooksanswers withcore.hooksPath, the dispatcher's own folder;--git-dirbreaks in a worktree, whose git dir holds no hooks. - Save stdin once and replay it to every step.
pre-pushgets its refs on stdin; without this the first check eats them and the repo's hook sees nothing. - Skip the repo hook when it is the dispatcher itself, so a repo whose hooks folder is the global one cannot loop.
Sample¶
input=$(mktemp); [ -t 0 ] || cat > "$input"
for check in "$here/$name.d"/*; do
[ -x "$check" ] && { "$check" "$@" < "$input" || exit 1; }
done
own="$(git rev-parse --git-common-dir)/hooks/$name"
[ -x "$own" ] && ! [ "$own" -ef "$0" ] && exec "$own" "$@" < "$input"
Install once:
A sample newfiles check refuses new files matching a pattern list or over
a size limit (tracked files can still change); secrets runs gitleaks when it
is installed. It pairs with addguard, which stops the sweeping
git add before it happens.
Limits¶
- A repo that sets its own
core.hooksPath(husky does) wins; the global checks do not run there. git lfs installcomplains "Hook already exists: pre-push" and exits 2; the filters are set and uploads still work through the dispatcher. Usegit lfs install --skip-repoto skip the warning.git commit --no-verifyskips every hook. These are guard rails against accidents; only a server-side check cannot be skipped.
Code¶
methods/githooks/ in the methods repository: the dispatcher (POSIX sh),
two sample checks, a README and a 20-check test in a sandboxed git config.