Skip to content

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 hooks answers with core.hooksPath, the dispatcher's own folder; --git-dir breaks in a worktree, whose git dir holds no hooks.
  • Save stdin once and replay it to every step. pre-push gets 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:

./githooks install ~/.config/git/hooks   # sets core.hooksPath globally

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 install complains "Hook already exists: pre-push" and exits 2; the filters are set and uploads still work through the dispatcher. Use git lfs install --skip-repo to skip the warning.
  • git commit --no-verify skips 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.