Git hooks are scripts Git runs automatically at certain points: before a commit is created, before a push, or after a server receives new commits. Teams use them to catch problems early (lint errors, secrets, bad commit messages) and, on servers, to trigger simple deployments.

Where hooks live

Each repository has .git/hooks/, with sample files (pre-commit.sample and others). A hook is just an executable file named after the event:

ls .git/hooks
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Hooks in .git/hooks aren’t committed, so they don’t travel with the repository. Sharing options are below.

Common client-side hooks

Hook Runs Typical use
pre-commit Before a commit is created Lint, format, block secrets or large files
prepare-commit-msg Before the message editor opens Insert a ticket number from the branch name
commit-msg After the message is written Enforce message conventions
pre-push Before pushing Run tests, block pushes to main
post-checkout, post-merge After switching branches or pulling Reinstall dependencies, regenerate files

A non-zero exit code aborts the action (for pre-* and commit-msg hooks).

Example: block accidental secrets and large files

#!/bin/sh
# .git/hooks/pre-commit
if git diff --cached -U0 | grep -E -i '(api[_-]?key|secret|password)\s*[:=]\s*["'"'"']?[A-Za-z0-9/+]{12,}' ; then
  echo "Possible secret in staged changes. Commit aborted."; exit 1
fi
git diff --cached --name-only -z | xargs -0 -r du -k 2>/dev/null | awk '$1 > 5120 {print "File too large: " $2; bad=1} END {exit bad}'

Dedicated tools (gitleaks, detect-secrets) are far better at finding secrets than a grep. See secrets management.

Example: enforce Conventional Commits

#!/bin/sh
# .git/hooks/commit-msg
if ! head -1 "$1" | grep -Eq '^(feat|fix|docs|chore|refactor|test|ci|perf)(\(.+\))?: .+'; then
  echo "Commit message must look like: feat(api): add export endpoint"; exit 1
fi

Share hooks with the team

Option 1: a committed hooks directory

mkdir .githooks && mv .git/hooks/pre-commit .githooks/
git config core.hooksPath .githooks       # each developer runs this once

Option 2: the pre-commit framework (language-agnostic, many ready-made hooks):

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks
pip install pre-commit
pre-commit install
pre-commit run --all-files

JavaScript projects often use Husky + lint-staged instead.

Client hooks can be skipped (git commit --no-verify), so run the same checks in CI as the real gate. See CI/CD pipeline explained.

Server-side hooks

On a server hosting a bare repository, hooks can enforce rules or deploy:

Hook Use
pre-receive Reject pushes (e.g. force pushes, policy violations)
update Per-branch checks
post-receive Notify, or deploy after a push

A minimal push-to-deploy setup for a static site:

# on the VPS
git init --bare /srv/git/site.git
cat > /srv/git/site.git/hooks/post-receive <<'EOF'
#!/bin/sh
GIT_WORK_TREE=/var/www/example.com git checkout -f main
EOF
chmod +x /srv/git/site.git/hooks/post-receive

# on your computer
git remote add live ssh://deploy@your-vps/srv/git/site.git
git push live main

For anything beyond simple sites, prefer CI/CD pipelines, which handle builds, tests and rollbacks. See WordPress Git deployment and GitHub Actions deploy to VPS. Hosted platforms (GitHub, GitLab, Gitea) offer webhooks and protected branches instead of custom server hooks.

Quick reference

Task How
Enable a hook Executable file in .git/hooks/<name>
Share hooks git config core.hooksPath .githooks
Framework pre-commit install
Skip once git commit --no-verify
Push-to-deploy post-receive in a bare repo

Related: Git branching strategies and Git commands cheat sheet.

  • Git
  • Automation
  • DevOps