Pre-commit Hooks for Code Quality
Pre-commit hooks run automatically before each git commit. They catch problems — formatting violations, linting errors, trailing whitespace, secrets accidentally staged — before they ever enter the commit history. The pre-commit Python framework makes managing hooks across a team effortless: one config file, one install command, and everyone runs the same checks.
Why use pre-commit hooks?
Catch style issues locally in milliseconds, not minutes later in CI.
Eliminate "fix linting" and "fix formatting" commits from your history.
Enforce consistent style without relying on everyone remembering to run formatters.
Block secrets (API keys, passwords) from being committed.
The
pre-commitframework handles installation, isolation, and updates automatically.
Install the pre-commit framework
Install pre-commit
# macOS brew install pre-commit # pip (any platform with Python) pip install pre-commit # With pipx (recommended for isolated install) pipx install pre-commit # Verify pre-commit --version # pre-commit 3.7.0
The .pre-commit-config.yaml file
Create .pre-commit-config.yaml in the root of your repository. This file defines which hooks to run. It is committed to version control so the whole team shares the same configuration.
.pre-commit-config.yaml — comprehensive example
repos:
# --- General file hygiene ---
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace # remove trailing spaces
- id: end-of-file-fixer # ensure files end with a newline
- id: check-yaml # validate YAML syntax
- id: check-json # validate JSON syntax
- id: check-toml # validate TOML syntax
- id: check-merge-conflict # block conflict markers
- id: check-added-large-files # warn on files > 500KB
args: ['--maxkb=500']
- id: detect-private-key # block private keys from being committed
- id: no-commit-to-branch # prevent direct commits to main
args: ['--branch', 'main', '--branch', 'master']
# --- JavaScript / TypeScript: Prettier ---
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, css, json, yaml, markdown]
# --- JavaScript / TypeScript: ESLint ---
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.57.0
hooks:
- id: eslint
files: \.([jt]sx?)$
types: [file]
additional_dependencies:
- eslint@8.57.0
- '@typescript-eslint/eslint-plugin@7.0.0'
- '@typescript-eslint/parser@7.0.0'
# --- Python: Black formatter ---
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
language_version: python3
# --- Python: Ruff linter (fast alternative to flake8) ---
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.1
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
# --- Python: mypy type checking ---
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-PyYAML]
# --- Secrets detection ---
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']Install hooks into your .git directory
Install and verify
# Install the hooks (run once per repo clone) pre-commit install # pre-commit installed at .git/hooks/pre-commit # Now hooks run automatically on every git commit git commit -m "test" # [INFO] Initializing environment for ... # Trim Trailing Whitespace...............Passed # End of files fixer.....................Passed # Check Yaml.............................Passed # Prettier...............................Passed # ESLint.................................Passed # [main abc1234] test
Running hooks manually
Manual hook runs
# Run all hooks against all files (useful for initial setup or CI) pre-commit run --all-files # Run a specific hook only pre-commit run prettier --all-files pre-commit run eslint --all-files # Run against specific files pre-commit run --files src/app.ts src/utils.ts # Update all hook versions to latest pre-commit autoupdate
Integrating with CI
Running pre-commit in CI catches violations that slipped through (e.g. teammates who bypassed hooks with --no-verify or have not installed hooks). The CI run uses --all-files to check everything.
.github/workflows/pre-commit.yml
name: Pre-commit
on:
pull_request:
push:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: pre-commit/action@v3.0.1Common hook configurations
Excluding files from specific hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-added-large-files
exclude: '^tests/fixtures/' # allow large test fixtures
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
exclude: '^(dist|.next|coverage)/' # skip generated filesRunning a hook only on specific file types
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
types: [python] # only runs on .py filesBypassing hooks in emergencies
Skip hooks when needed
# Skip all pre-commit hooks for a single commit git commit --no-verify -m "emergency fix: skip hooks" # Skip a specific hook SKIP=eslint git commit -m "WIP: skip only eslint"
Comparison: frameworks for managing hooks
Feature | pre-commit framework | Husky (Node) | Shell scripts in .git/hooks |
|---|---|---|---|
Language | Python (manages any language hook) | Node.js / npm | Bash |
Team sharing | Yes — .pre-commit-config.yaml in repo | Yes — .husky/ in repo | No — .git/ is not committed |
Isolated environments | Yes — each hook in its own venv | No — uses global install | No |
Hook sources | GitHub repos, local, Docker | npm packages | Any script |
Version pinning | Yes — pin rev per hook | Via npm versions | Manual |
CI integration | Official GitHub Action available | Run via npm scripts | Any script runner |
Best for | Polyglot projects, security-focused | Node.js-only projects | Quick one-off hooks |
Setting up a new team member
Onboarding steps after cloning a repo with pre-commit
# After cloning the repo git clone https://github.com/org/repo.git cd repo # Install pre-commit (if not already installed) pip install pre-commit # Install the hooks (this is the only per-clone setup step) pre-commit install # Run against all files to confirm everything passes pre-commit run --all-files # All checks pass — you are ready to commit