pre-commit
Installation
# pip (recommended)
pip install pre-commit
# pipx (isolated, no venv needed)
pipx install pre-commit
# Homebrew
brew install pre-commit
# conda
conda install -c conda-forge pre-commit
# Verify
pre-commit --version
Install the git hook into your repository:
# Run from repo root — installs .git/hooks/pre-commit
pre-commit install
# Also install commit-msg hook (for commitlint etc.)
pre-commit install --hook-type commit-msg
# Install pre-push hook
pre-commit install --hook-type pre-push
Configuration
Create .pre-commit-config.yaml in your repo root:
Minimal Config
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
Full-Featured Python Config
default_language_version:
python: python3.12
default_stages: [pre-commit]
fail_fast: false
repos:
# General file hygiene
- 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
args: [--allow-multiple-documents]
- id: check-toml
- id: check-json
- id: check-merge-conflict
- id: check-added-large-files
args: [--maxkb=1000]
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main, --branch, master]
- id: mixed-line-ending
# Python formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
# Python type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-pyyaml]
# Security scanning
- repo: https://github.com/PyCQA/bandit
rev: 1.7.8
hooks:
- id: bandit
args: [-r, src/]
exclude: tests/
# Secret detection
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: [--baseline, .secrets.baseline]
# Commit message linting
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.24.0
hooks:
- id: commitizen
stages: [commit-msg]
# Markdown linting
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.40.0
hooks:
- id: markdownlint
args: [--fix]
JavaScript / Node Config
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-json
- id: detect-private-key
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.2.0
hooks:
- id: eslint
files: \.(js|jsx|ts|tsx)$
additional_dependencies:
- eslint@9.2.0
- eslint-plugin-react@7.34.1
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
files: \.(js|jsx|ts|tsx|json|css|scss|md|yaml)$
Core Commands
| Command | Description |
|---|---|
pre-commit install | Install hooks into .git/hooks/ |
pre-commit uninstall | Remove hooks from .git/hooks/ |
pre-commit run | Run hooks on staged files |
pre-commit run --all-files | Run hooks on all repo files |
pre-commit run <hook-id> | Run a specific hook only |
pre-commit run --files a.py b.py | Run on specific files |
pre-commit autoupdate | Bump all hook revisions to latest |
pre-commit autoupdate --repo https://... | Update a single repo |
pre-commit clean | Clear hook environments cache |
pre-commit gc | Garbage-collect unused envs |
pre-commit validate-config | Validate .pre-commit-config.yaml |
pre-commit validate-manifest | Validate .pre-commit-hooks.yaml |
pre-commit sample-config | Print a starter config |
pre-commit try-repo <url> | Test a hook repo without installing |
pre-commit try-repo <url> <hook-id> | Test a specific hook |
Skip and Override
| Command | Description |
|---|---|
SKIP=flake8 git commit | Skip a specific hook by ID |
git commit --no-verify | Skip all hooks for this commit |
PRE_COMMIT_ALLOW_NO_CONFIG=1 git commit | Allow commit with no config |
Advanced Usage
Local Hooks (No Remote Repo Needed)
repos:
- repo: local
hooks:
# Run a shell script
- id: run-tests
name: Run unit tests
entry: bash -c 'pytest tests/ -x -q'
language: system
pass_filenames: false
stages: [pre-push]
# Run a Python script
- id: check-migrations
name: Check for missing migrations
entry: python manage.py makemigrations --check
language: system
pass_filenames: false
types: [python]
# Run a Node script
- id: lint-staged
name: ESLint staged files
entry: npx lint-staged
language: system
pass_filenames: false
# Inline script
- id: no-debug-statements
name: No debug statements
entry: grep -rn 'import pdb\|breakpoint()\|console\.log'
language: system
types: [python, javascript]
pass_filenames: true
Language-Specific Environments
repos:
- repo: local
hooks:
# Uses the repo's own virtualenv
- id: mypy
name: mypy type check
entry: mypy
language: python
additional_dependencies: [mypy==1.10.0]
types: [python]
# Golang
- id: go-vet
name: go vet
entry: go vet ./...
language: golang
pass_filenames: false
# Docker image
- id: hadolint
name: Lint Dockerfile
entry: hadolint
language: docker_image
types: [dockerfile]
CI Integration
Run pre-commit in GitHub Actions without installing hooks:
# .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.1
Or manually in any CI:
# Install and run without git hooks
pip install pre-commit
pre-commit run --all-files
Pinning with --freeze
# Generate a frozen lockfile with exact SHAs
pre-commit autoupdate --freeze
This produces:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 # frozen: sha256:...
Caching in CI
# GitHub Actions cache
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
exclude and files Patterns
hooks:
- id: trailing-whitespace
exclude: '^(docs/|\.github/)' # regex, excludes paths
files: '\.py$' # only run on Python files
exclude_types: [markdown] # exclude by file type
types_or: [python, javascript] # run on py OR js
Hook Stages
hooks:
- id: pytest
stages: [pre-push] # only on push, not commit
- id: commitizen
stages: [commit-msg] # runs on commit message
- id: ruff
stages: [pre-commit, manual] # manual = pre-commit run <id>
Common Workflows
Bootstrap a New Repo
# 1. Install pre-commit
pip install pre-commit
# 2. Generate a starter config
pre-commit sample-config > .pre-commit-config.yaml
# 3. Edit the config with your hooks
# 4. Install the git hook
pre-commit install
# 5. Run on all files to catch existing issues
pre-commit run --all-files
# 6. Commit the config
git add .pre-commit-config.yaml
git commit -m "chore: add pre-commit hooks"
Update All Hooks
# Bump all revs to latest tags
pre-commit autoupdate
# Review the diff
git diff .pre-commit-config.yaml
# Commit
git add .pre-commit-config.yaml
git commit -m "chore: update pre-commit hooks"
Debug a Failing Hook
# Run only the failing hook verbosely
pre-commit run <hook-id> --all-files --verbose
# Try a hook from any repo without committing
pre-commit try-repo https://github.com/astral-sh/ruff-pre-commit ruff --all-files
# Inspect hook environment
pre-commit run --show-diff-on-failure ruff
Migrate from lint-staged
# lint-staged runs per staged file — replicate with:
repos:
- repo: local
hooks:
- id: eslint
name: ESLint
entry: eslint --fix
language: system
types: [javascript, jsx, ts, tsx]
# pre-commit passes only staged files by default
Tips and Best Practices
- Commit
.pre-commit-config.yamlinto version control so the whole team shares the same hooks. - Pin
rev:to a tag or SHA, neverrev: main— this ensures reproducible environments. - Run
pre-commit run --all-filesafter adding new hooks to fix existing issues before they block future commits. - Use
fail_fast: truelocally for faster feedback when you know a hook will fail; usefalsein CI to see all issues at once. SKIP=hook-id git commitlets you bypass one hook without disabling all hooks with--no-verify.- Cache the pre-commit environment in CI (
~/.cache/pre-commit) to avoid re-downloading hooks on every run. pre-commit autoupdateshould be a periodic maintenance task — schedule it monthly or run it in Dependabot.- Local hooks are great for project-specific checks (custom linters, migration checks, test runs on push) without publishing a hook repo.
stages: [pre-push]for slow hooks (tests, type checking) so they don’t block every commit.pre-commit gcremoves cached environments for hooks that are no longer in your config — run it after major config changes.detect-private-keyanddetect-secretsare essential security hooks — include them in every project.no-commit-to-branchprevents accidental direct commits tomainormaster— a simple but valuable guard.