Client-side hooks
Hooks are executable scripts in .git/hooks/ (or wherever core.hooksPath points). Git invokes them at well-defined moments. The three commit-time hooks let you enforce policy before a commit is finalized.
pre-commit
Runs before the commit message is composed. Exit non-zero to abort. Typical uses: linting, formatting, secret scanning, running fast tests.
#!/usr/bin/env bash
# .git/hooks/pre-commit
set -e
files=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.py$' || true)
[ -z "$files" ] && exit 0
ruff check $files
ruff format --check $files
Make it executable: chmod +x .git/hooks/pre-commit.
prepare-commit-msg
Runs after the default message is generated and before the editor opens. Useful for prepending ticket numbers extracted from the branch name.
#!/usr/bin/env bash
# .git/hooks/prepare-commit-msg
MSG_FILE=$1
SOURCE=$2
[ "$SOURCE" = "message" ] && exit 0 # -m given, no editor
branch=$(git symbolic-ref --short HEAD)
ticket=$(echo "$branch" | grep -oE '[A-Z]+-[0-9]+' || true)
[ -n "$ticket" ] && sed -i.bak "1s/^/[$ticket] /" "$MSG_FILE"
commit-msg
Runs after the user has written the message. Use it to validate format — for example, enforcing Conventional Commits.
#!/usr/bin/env bash
# .git/hooks/commit-msg
pattern='^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .{1,72}'
grep -qE "$pattern" "$1" || {
echo "Commit message must follow Conventional Commits."
exit 1
}
Sharing hooks with the team
Hooks under .git/hooks/ are not version-controlled. Use core.hooksPath to point at a tracked directory:
git config core.hooksPath .githooks
Or use a manager like pre-commit (Python), lefthook, or husky (JS) to install hooks from a config file.
Bypassing
Pass --no-verify to skip pre-commit and commit-msg hooks. Reserve this for emergencies; CI should re-check.
Common mistakes
Slow hooks frustrate developers and get bypassed. Keep them under a second when possible. Hooks that mutate the working tree without staging the changes confuse Git. Forgetting executable bit on Windows checkouts breaks hook execution; check with git ls-files --stage .githooks.
Related
See "Git hooks: post-commit, post-receive, pre-push" for after-commit hooks, and "Removing sensitive data from history" for secret scanning patterns.