Skip to content

Pre-commit hooks, configuration, workflows, and CI reference.

pre-commit is a framework for running automated checks before code enters your Git history. It can format files, catch simple mistakes, validate configuration, and run project tools such as Ruff, Black, or mypy.

A Git hook is a command that runs at a Git lifecycle event. The most common event is pre-commit, which runs after git commit is requested but before the commit is created.

edit files -> git add -> git commit -> hooks run -> commit is created
-> failure: fix and stage changes again

Use hooks to catch repeatable issues early. Keep expensive integration tests in CI unless the team intentionally wants them during local commits.

Install pre-commit in an isolated tool environment where possible:

Terminal window
pipx install pre-commit # Recommended for a globally available CLI
uv tool install pre-commit # Install with uv
python -m pip install pre-commit # Install into the active Python environment
pre-commit --version # Verify installation

Hooks are declared in .pre-commit-config.yaml at the root of the repository. Start with a sample configuration or create one manually:

Terminal window
pre-commit sample-config > .pre-commit-config.yaml

A useful starter configuration:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- id: debug-statements

Each repository is pinned to a rev so every developer and CI run uses the same hook version.

After the configuration file exists, install it into the local Git repository:

Terminal window
pre-commit install # Install the pre-commit hook
pre-commit install --install-hooks # Install hook script and environments now
git add .pre-commit-config.yaml
git commit -m "Configure pre-commit hooks"

The first hook run may take longer because pre-commit builds isolated environments for the configured tools.

Once installed, hooks run automatically on staged files:

Terminal window
git add app.py
git commit -m "Add application logic"

If a formatter modifies a file or a check fails:

Terminal window
git diff # Review fixes made by hooks
git add . # Stage the corrected files
git commit -m "Add application logic" # Try the commit again

Run checks before committing or across existing files:

Terminal window
pre-commit run # Run hooks on staged files
pre-commit run --all-files # Run every hook on every tracked file
pre-commit run trailing-whitespace # Run one hook
pre-commit run --files app.py test_app.py
pre-commit run --show-diff-on-failure

--all-files is useful when first adding hooks to an existing repository because older files are not normally checked until they are changed.

Use verbose output and configuration validation when diagnosing failures:

Terminal window
pre-commit run --all-files --verbose
pre-commit validate-config
pre-commit validate-manifest # For repositories that publish hooks
pre-commit clean # Remove cached hook environments

The pre-commit-hooks project contains general checks that work in many codebases:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-ast # Validate Python syntax
- id: check-json
- id: check-toml
- id: check-yaml
- id: check-merge-conflict
- id: check-added-large-files
args: ["--maxkb=1000"]
- id: detect-private-key

Choose hooks that enforce project rules without surprising contributors. For example, end-of-file-fixer and trailing-whitespace change files automatically, while check-yaml only reports invalid YAML.

Ruff can replace several fast Python formatting and linting steps:

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format

Put ruff-check --fix before ruff-format so automatically fixed imports or code are formatted afterward. Update pinned revisions regularly rather than assuming sample revisions are the newest releases.

Projects already using Black and isort can run their established tools:

repos:
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.10.0
hooks:
- id: black
language_version: python3

Configure these tools in pyproject.toml so local hooks, editors, and CI use one shared rule set:

[tool.black]
line-length = 88
[tool.isort]
profile = "black"

Type checking often needs the project dependencies or type stubs:

repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies:
- types-requests

For applications with complex imports or plugins, a local hook using the project’s managed environment may be easier to keep consistent than a separate hook environment.

repos:
- repo: https://github.com/example/tool
rev: v1.2.3 # Pinned tag or commit
hooks:
- id: tool-check # Hook supplied by the repository
name: run tool check # Optional display name
args: [--fix] # Extra CLI arguments
files: '\.py$' # Include matching paths
exclude: '^generated/' # Exclude matching paths
stages: [pre-commit] # Git stage at which it runs
additional_dependencies: [] # Packages added to its environment

Common top-level defaults:

default_install_hook_types: [pre-commit, pre-push]
default_stages: [pre-commit]
fail_fast: false
minimum_pre_commit_version: "3.0.0"

Filter hooks with regular expressions:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
files: ^(config|deploy)/.*\.ya?ml$
exclude: ^config/generated/

Use a repository-wide exclusion for generated or vendored content:

exclude: |
(?x)^(
migrations/|
vendor/|
dist/|
docs/generated/
)

Hook-specific command arguments are supplied as YAML arrays:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: [--maxkb=500]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]

Install additional Git hook types and assign expensive checks only where they belong:

default_install_hook_types: [pre-commit, pre-push]
repos:
- repo: local
hooks:
- id: unit-tests
name: run unit tests before push
entry: python -m pytest
language: system
pass_filenames: false
stages: [pre-push]
Terminal window
pre-commit install --hook-type pre-commit
pre-commit install --hook-type pre-push
pre-commit run --hook-stage pre-push --all-files

Fast formatting and validation checks suit pre-commit. Longer tests may suit pre-push or CI.

Create hooks that are available on demand but do not slow normal commits:

repos:
- repo: local
hooks:
- id: full-test-suite
name: run full test suite
entry: python -m pytest
language: system
pass_filenames: false
stages: [manual]
Terminal window
pre-commit run --hook-stage manual full-test-suite --all-files

Use repo: local for commands defined by the current project:

repos:
- repo: local
hooks:
- id: pytest
name: pytest
entry: python -m pytest
language: system
pass_filenames: false
types: [python]
- id: compile-python
name: compile changed Python files
entry: python -m py_compile
language: system
types: [python]

language: system uses tools from the active environment. This is convenient, but each contributor and CI job must install those dependencies first.

By default, pre-commit passes matching changed filenames to a hook. Disable that behavior for commands that discover files themselves:

repos:
- repo: local
hooks:
- id: django-check
name: Django system checks
entry: python manage.py check
language: system
pass_filenames: false
always_run: true

Pinned hook versions make builds repeatable. Upgrade them intentionally:

Terminal window
pre-commit autoupdate # Update all hook revisions
pre-commit autoupdate --repo https://github.com/pre-commit/pre-commit-hooks
pre-commit run --all-files # Test updated hooks
git diff .pre-commit-config.yaml # Review version changes

Commit hook upgrades separately when possible so formatting changes and tool-version changes are easy to review.

CI should run hooks on all tracked files so contributors who did not install local hooks still receive the same checks:

Terminal window
pre-commit run --all-files --show-diff-on-failure

Example GitHub Actions workflow:

name: pre-commit
on:
pull_request:
push:
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

CI normally reports hook fixes rather than committing changes back to a pull request. Contributors can run the hooks locally, stage fixes, and push again.

Temporarily skip a known hook only when there is a clear reason:

Terminal window
SKIP=mypy git commit -m "Work in progress" # macOS/Linux
$env:SKIP="mypy"; git commit -m "Work in progress" # PowerShell
Remove-Item Env:SKIP # Clear in PowerShell

Git provides an escape hatch:

Terminal window
git commit --no-verify -m "Emergency commit"

Use bypasses sparingly. The same hooks should run in CI, where skipped problems can still block merging.

A modifying hook fails the current commit after it makes fixes. Review and stage the fixes, then commit again:

Terminal window
pre-commit run --all-files
git diff
git add .
git commit -m "Apply formatting fixes"

Clear cached environments and rebuild them:

Terminal window
pre-commit clean
pre-commit install --install-hooks
pre-commit run --all-files

Check that you are in a Git repository, the hook is installed, and its file filters match your staged changes:

Terminal window
git rev-parse --show-toplevel
pre-commit install
pre-commit run --all-files --verbose

Introduce formatting changes in a dedicated commit before feature work:

Terminal window
pre-commit run --all-files
git add .
git commit -m "Apply pre-commit formatting"

This keeps later reviews focused on behavior changes rather than baseline cleanup.

A practical adoption sequence for a Python repository:

Terminal window
pipx install pre-commit
pre-commit sample-config > .pre-commit-config.yaml
# Edit the configuration to select project hooks
pre-commit install
pre-commit run --all-files
git add .pre-commit-config.yaml
git add <files-updated-by-hooks>
git commit -m "Add pre-commit checks"

For a mature team:

  1. Begin with fast whitespace, syntax, and configuration checks.
  2. Add the formatter and linter already used by the project.
  3. Run all hooks in CI.
  4. Add slower hooks only when their feedback is worth the local delay.
  5. Update pinned hook revisions regularly and review resulting fixes.
Terminal window
pre-commit install # Enable hooks in this clone
pre-commit install --hook-type pre-push # Enable push-stage hooks
pre-commit run # Run against staged files
pre-commit run --all-files # Run against all tracked files
pre-commit run <hook-id> --all-files # Run one hook everywhere
pre-commit run --hook-stage manual <id> --all-files
pre-commit autoupdate # Upgrade pinned hook revisions
pre-commit validate-config # Validate YAML configuration
pre-commit clean # Rebuild cached hook environments
git commit --no-verify # Bypass hooks temporarily