By admin , 29 April 2026

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.