Git Hooks
Hooks are scripts git runs automatically at specific points in the workflow. Used to enforce style, run tests, prevent bad commits, validate messages, sync external systems.
Two flavors:
- Client-side: run on developer machines (
pre-commit,commit-msg,pre-push). - Server-side: run on the central git host (
pre-receive,update,post-receive).
Where hooks live
.git/hooks/
├── pre-commit.sample
├── commit-msg.sample
├── pre-push.sample
├── post-merge.sample
└── ...
Sample files are inert (.sample suffix). Rename to the hook name (no extension) and chmod +x to activate.
Common client-side hooks
| Hook | Fires | Common use |
|---|---|---|
pre-commit |
before git commit writes the commit |
run linter/formatter/tests, block bad code |
prepare-commit-msg |
before opening commit-message editor | inject branch name, ticket ID |
commit-msg |
after writing commit message, before completing commit | enforce message format (Conventional Commits) |
pre-push |
before git push sends to remote |
run tests, block protected branches |
post-merge |
after git merge completes |
re-install dependencies if requirements.txt changed |
post-checkout |
after git switch/checkout |
refresh virtualenv, db migrations |
Hooks exit non-zero to abort the operation.
A minimal pre-commit:
#!/bin/bash
ruff check . || exit 1
pytest -x --quiet || exit 1
chmod +x .git/hooks/pre-commit. Now every git commit runs ruff and pytest first. Failure aborts the commit.
The big problem with .git/hooks/
It’s not in version control. Each developer would have to set up hooks individually, and there’s no enforcement.
pre-commit framework — the de facto standard
pre-commit (the Python package, not the git hook) manages hooks via a checked-in config file. Set up once, every dev gets the same hooks.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-yaml
- id: check-added-large-files
args: [--maxkb=500]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
Setup:
pip install pre-commit
pre-commit install # writes a .git/hooks/pre-commit that runs the framework
Now every commit runs the configured hooks. The same config file works in CI:
pre-commit run --all-files
Useful built-in checks:
check-merge-conflict— blocks committing files with<<<<<<<markers.check-added-large-files— blocks accidental binary blobs.detect-private-key— refuses commits containing private keys.check-yaml,check-json,check-toml— syntax-validate configs.
Conventional Commits enforcement
commit-msg hook to require feat: / fix: / chore: prefixes:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.2.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
Pairs with semantic-release / changelog tooling.
Bypassing hooks
git commit --no-verify # skip pre-commit and commit-msg
git push --no-verify # skip pre-push
Useful for emergencies. But: in CI, run pre-commit run --all-files so bypassed-locally bad commits get caught before merge.
The CLAUDE.md rule “never skip hooks unless explicitly asked” applies to working with AI agents too — bypassing hooks defeats their purpose.
Server-side hooks
Run on the git server (you don’t have these on GitHub/GitLab/Bitbucket SaaS — they replace them with branch protection rules and CI integrations).
| Hook | Fires |
|---|---|
pre-receive |
before any ref update; can reject the entire push |
update |
per-ref version of pre-receive |
post-receive |
after refs updated; for notifications, deploys |
Self-hosted GitLab/Gitea support custom server-side hooks. SaaS git hosts replace this with:
- Branch protection (required reviews, required CI).
- GitHub Actions / GitLab CI as policy enforcement.
Husky (JS/TS world)
husky is the Node/JS equivalent of Python’s pre-commit framework. Same idea: checked-in config, installed via npm install, manages .git/hooks/. Common in mixed Python/JS repos for the JS side.
Hook performance
Slow hooks make commits annoying. Tips:
- Run only on staged files:
pre-commitframework does this by default. - Heavy checks (full test suite, mypy on the whole repo) →
pre-pushinstead ofpre-commit. - Truly slow checks → CI only.
A 30-second pre-commit will cause people to use --no-verify or commit less often. Sub-3-second is the goal.
Hooks and submodules / worktrees
Hooks live in .git/hooks/ of the main repo. Worktrees share the same .git/, so they share hooks. Submodules have their own hooks.
Common interview confusions
- “Hooks are version-controlled.” —
.git/hooks/is not. Thepre-commitframework’s.pre-commit-config.yamlis, which is the workaround. - “Server-side hooks work on GitHub.” — you can’t add server-side hooks to GitHub/GitLab.com. Use branch protection + Actions/CI instead. Self-hosted GitLab/Gitea allow custom server hooks.
- “
--no-verifyis fine to use freely.” — it bypasses safety nets the team agreed on. Use rarely; rely on CI to catch what was bypassed.
Interview angle
- “What are git hooks and what do you use them for?” — scripts git runs at specific points. Common:
pre-commit(lint/format),commit-msg(enforce message format),pre-push(run tests),post-checkout(refresh deps). - “Why use the
pre-commitframework instead of writing hooks in.git/hooks/?” — hooks aren’t version-controlled and aren’t shared with the team. The framework uses a checked-in YAML config that everyone installs once. - “Common pre-commit checks?” — formatter (ruff/black), linter (ruff/flake8), type check (mypy), trailing whitespace, large file blocker, private-key detector, conflict-marker detector.
- “What do you put in
pre-commitvspre-pushvs CI?” — fast/local-only stuff in pre-commit (formatters, basic lint), slower stuff in pre-push (full test on small project), heavy stuff (full test suite, integration tests, security scans) in CI. - “How do you enforce hooks on a SaaS git host like GitHub?” — you can’t run server-side hooks. Use branch protection (required reviews, required status checks) and GitHub Actions to enforce policy.
- “Someone bypasses pre-commit with
--no-verify— what stops bad code from landing?” — CI re-runs the same hooks viapre-commit run --all-files. Branch protection requires CI to pass before merge.