Why pre-commit hooks
The cheapest place to catch a lint error is on the developer's machine, before the commit even forms. Pre-commit hooks run lint, format, and quick checks; if they fail, the commit is rejected. CI catches what slips through, but pre-commit catches the 95% of issues that should never reach CI.
The simplest hook
# .git/hooks/pre-commit
#!/usr/bin/env bash
set -e
npm run lint --silent
npm run format:check --silent
chmod +x .git/hooks/pre-commit
Now git commit runs both checks; failures abort.
Linting only changed files
#!/usr/bin/env bash
set -e
files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\\.(js|ts|tsx)$' || true)
if [ -z "$files" ]; then
exit 0
fi
echo "$files" | xargs npx eslint --max-warnings=0
echo "$files" | xargs npx prettier --check
Linting only the staged files keeps hooks fast - critical, because slow hooks get bypassed.
Husky for Node projects
npm install --save-dev husky lint-staged
npx husky init
# .husky/pre-commit
npx lint-staged
# package.json
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"]
}
Husky checks itself in via .husky/ so the whole team gets the hooks on npm install. lint-staged runs commands only on staged files.
The pre-commit framework (Python ecosystem)
# .pre-commit-config.yaml
repos:
- 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
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
- id: ruff-format
pip install pre-commit
pre-commit install
This framework supports any language and is the de facto choice for Python codebases.
Sharing hooks via core.hooksPath
Without an installer, point Git at a tracked directory:
git config core.hooksPath .githooks
Now hooks live in .githooks/ and are part of the repo. Each developer runs the config command once.
Bypassing when necessary
git commit --no-verify -m "WIP"
Sometimes you need to bypass - emergency hotfix, work-in-progress checkpoint. Document the policy and rely on CI as the authoritative gate.
Common pre-commit checks
- Lint and format.
- Type checks (TypeScript, mypy).
- Trailing whitespace, end-of-file newlines.
- Merge conflict markers.
- Secret detection (
gitleaks,trufflehog). - File size limits.
Keeping hooks fast
A hook that takes 30 seconds will be bypassed within a week. Aim for under 5 seconds. Run only on changed files. Cache aggressively. If something is slow, move it to CI and accept the trade-off.
Pre-commit hooks turn the local git commit into a quality gate. Set them up once per repo; reap the benefits forever.