Git Hooks: Automating Checks with pre-commit and Husky
Advanced4 minInnovialabs editorial team
In short
Git hooks are scripts Git runs before a commit or push; the pre-commit framework (Python) and Husky with lint-staged (JS) share them across the team and check only the changed files.
The short answer
Git hooks are scripts that Git runs by itself at specific moments: before a commit, when the commit message is written, before a push. If a script exits with an error, the action is cancelled.
The problem with "bare" hooks: they live in .git/hooks, and that folder is not part of the repository. So teams use tools that keep the configuration in code and install the hooks for every developer:
- pre-commit — a Python framework that works with any language, popular in Python projects.
- Husky + lint-staged — the standard pairing in JavaScript and TypeScript projects.
Useful client-side hooks
| Hook | When it runs | What is usually checked |
|---|---|---|
pre-commit | before the commit is created | linter, formatting, secrets in code |
commit-msg | after the message is entered | commit message format |
pre-push | before sending to the server | quick tests, type checks |
The key principle: a hook must be fast. If a commit takes a minute, developers will start bypassing the checks.
Python: the pre-commit framework
Install the package and create .pre-commit-config.yaml in the project root:
default_install_hook_types: [pre-commit, commit-msg]
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-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
Then:
pip install pre-commit
pre-commit install # installs hooks into .git/hooks
pre-commit run --all-files # first run over the whole project
pre-commit autoupdate # bump the versions in rev
What to know:
- Each hook runs in its own isolated environment with versions pinned in
rev, so the whole team gets identical results. - By default only the files in the commit are checked.
- Here
ruffcovers both linting and formatting; you can addblack,isort,mypyand others.
JavaScript: Husky and lint-staged
Husky manages the hooks, and lint-staged runs commands only on files added to the index.
npm install --save-dev husky lint-staged
npx husky init
husky init creates a .husky folder and adds a prepare script so hooks are installed automatically after npm install. Replace the contents of .husky/pre-commit with:
npx lint-staged
lint-staged configuration in package.json:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,md,json}": "prettier --write"
}
}
Checking commit messages with commitlint:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
Now a message like fix: correct discount calculation passes, while fixes does not. The commitlint config format depends on whether the project uses ES modules; check the documentation for your version.
Hooks do not replace CI
Any hook can be skipped with git commit --no-verify, and a new developer may simply not have them installed. So:
- the same checks are duplicated in CI — that is the last line of defence;
- hooks give fast feedback before a push, not a guarantee;
- keep heavy checks (the full test suite, builds) in CI or in
pre-push.
Common mistakes
- Running the linter over the whole project on every commit — slow. Check only changed files.
- Not pinning tool versions — different people get different results.
- Turning on strict rules at once in a legacy project — hundreds of errors, and the hooks get disabled. Start with formatting and add rules gradually.
- Forgetting about Windows: hook scripts must work in Git Bash, so avoid platform-specific commands.