Andrew Mercer
on this page

08 · Hooks

← Overview · Prev: Branch management · Next: Internals →

Hooks are executable scripts git runs at specific points. They live in .git/hooks/ (or the directory set by core.hooksPath). They are local: not part of the repo, not pushed. Every developer must install them, or use a shared directory.

Hook Runs Typical use Can abort?
pre-commit before the commit is recorded formatters, linters (cargo fmt, terraform fmt) yes
prepare-commit-msg before the editor opens pre-fill message (e.g. ticket key) yes
commit-msg after the message is written; gets the message file as $1 enforce message format yes
post-commit after the commit notifications no
pre-rebase before a rebase block rebasing published branches yes
post-merge / post-checkout after those operations reinstall deps, regenerate files no
pre-push before git push; receives refs on stdin tests, version-bump checks, block pushes to main yes

Bypass (use sparingly): git commit --no-verify, git push --no-verify.

Use one shared hooks directory for all repos

mkdir -p ~/.local/git_hooks
git config --global core.hooksPath ~/.local/git_hooks
chmod +x ~/.local/git_hooks/*

Note: when core.hooksPath is set, each repo's own .git/hooks/ is ignored. Symlink or copy the hooks you need from one master directory so edits apply everywhere.

git-adm: git-adm hooks path (set the shared dir), git-adm hooks list (see what is installed and executable).

commit-msg: enforce PROJ-123456 - message

Message-format checks belong in commit-msg (it receives the message), not pre-commit.

#!/usr/bin/env bash
# ~/.local/git_hooks/commit-msg   (chmod +x)
msg_file="$1"
subject="$(head -n1 "$msg_file")"

# Let git-generated messages through
case "$subject" in Merge\ *|Revert\ *|fixup!\ *|squash!\ *) exit 0 ;; esac

pattern='^[A-Z][A-Z0-9]*-[0-9]+ - .+'
if [[ ! "$subject" =~ $pattern ]]; then
  cat >&2 <<'EOF2'
Commit message format invalid.
Expected:  PROJ-123456 - descriptive message
Example:   PROJ-123456 - Fix user login bug
EOF2
  exit 1
fi

Test it: git commit -m "Fix bug" → rejected; git commit -m "PROJ-123456 - Fix bug" → accepted.

git-adm: git-adm hooks install-commit-msg [--force] writes this hook (pattern from key_pattern in ~/.git-adm.conf).

pre-commit: fail on formatting problems

#!/usr/bin/env bash
# Example for a Rust project
[ "${SKIP_FMT:-}" = "1" ] && exit 0
cargo fmt --check && cargo clippy -- -D warnings

pre-push: require a version file bump

Fails the push if a top-level directory changed but its version file did not.

#!/usr/bin/env bash
# ~/.local/git_hooks/pre-push   (chmod +x)
[ "${GIT_NO_VERSION_CHECK:-}" = "true" ] && exit 0

git fetch origin main --quiet
base="$(git merge-base HEAD origin/main)"
changed_files="$(git diff --name-only "$base"...HEAD)"
changed_dirs="$(awk -F/ 'NF>1 {print $1}' <<<"$changed_files" | sort -u)"

rc=0
for dir in $changed_dirs; do
  [ -f "$dir/version" ] || continue
  if ! grep -qx "$dir/version" <<<"$changed_files"; then
    echo "version not bumped in: $dir/version" >&2
    rc=1
  fi
done
exit "$rc"

git-adm push performs an equivalent check itself and sets GIT_NO_VERSION_CHECK=true so this hook is skipped.

Sharing hooks with a team

Because hooks aren't versioned, commit them to the repo (e.g. .githooks/) and point git at them:

git config core.hooksPath .githooks

Or use a framework such as pre-commit (https://pre-commit.com).

Reading: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks