cloudparts

pre-commit

A package manager for Git hooks: you list linters and formatters by repository and version in one YAML file, and it installs each into its own isolated environment and runs it on the staged files before every commit.

What it is
Git hooks
Type
Open source
Made by
pre-commit dev team (Anthony Sottile, Ken Struys)
License
MIT
Facts checked
13 October 2026

What it is

A project lists its hooks in .pre-commit-config.yaml: each entry names a Git repository, a rev (a tag or commit) and the hook ids to use from it. pre-commit install writes a small script to .git/hooks/pre-commit. On git commit, that script reads the config, and the first time it meets a hook repository it clones it at that revision and builds an environment for it in the hook’s own language (it supports 21 language types, from Python virtualenvs and Node to Rust and Docker images), cached under ~/.cache/pre-commit. If a developer has no Node installed, pre-commit downloads and builds it, without root.

Each run checks only what is about to be committed. pre-commit stashes unstaged changes, passes the staged file names to each hook that matches by path pattern or file type, and restores the stash afterward. A hook fails the commit by exiting nonzero or by modifying files, so a formatter that rewrites a file stops the commit until you stage its fix. The same config also drives other Git hooks (commit-msg, pre-push, post-checkout and six more, selected with stages), and pre-commit run --all-files runs every hook over the whole tree, which is how it is used in CI.

Pinning rev makes every developer and CI job run the same linter version, and pre-commit autoupdate bumps each pin to the latest tag. A hook repository only needs a .pre-commit-hooks.yaml manifest that says the hook’s id, entry command, language and file types. The separate pre-commit.ci service runs the config on GitHub pull requests, pushes fixes back to them and sends weekly autoupdate pull requests; it is free for open-source repositories.

Why teams pick it

  • Language-agnostic: a Node project can run a Ruby or Rust linter without adding a Gemfile or Cargo setup, because each hook gets its own isolated environment
  • Versions are pinned per hook in one file, so local commits and CI run identical tools
  • Runs only on staged files by default, stashing the rest, so a check never passes because of an edit you haven’t staged
  • Large ecosystem: projects such as Ruff ship an official hook repository you add in three lines

Watch out for

  • Hooks run only on machines where someone ran pre-commit install, and git commit --no-verify skips them, so CI still has to run pre-commit run --all-files
  • The first run per hook clones and builds its environment, which can take minutes (building Node or Ruby from scratch), and the cache grows until you run pre-commit gc
  • pre-commit itself needs Python 3.11 or newer on every machine, even in projects that don’t otherwise use Python
  • Hooks that read the whole project (type checkers, test suites) don’t fit the file-by-file model well and slow every commit; the manual and pre-push stages exist for them

How it fits with other technologies

.pre-commit-config.yaml, then `pre-commit install` once per clone · yaml

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.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.17.0
    hooks:
    -   id: ruff-check
        args: [--fix]
    -   id: ruff-format

# pre-commit install            -> .git/hooks/pre-commit
# pre-commit run --all-files    -> every hook over the whole tree (CI)
# pre-commit autoupdate         -> bump each rev to its latest tag
# SKIP=ruff-check git commit    -> skip one hook instead of --no-verify

See it in a real stack

No architecture uses pre-commit yet. Drop it into the playground and wire it up yourself.Open playground →
Where it came from2014–2026

Yelp relied on Git pre-commit hooks to catch trivial problems such as missing semicolons, whitespace and leftover debug statements before code review, so reviewers could look at the design of a change instead. As the company created more libraries, sharing those hooks became painful: engineers copied bash scripts from project to project and edited them by hand for each layout.

Anthony Sottile and Ken Struys started pre-commit in March 2014 to fix that. Their premise was that a project should use the best linter for each file type even when it is written in a language the project doesn’t use, such as the Ruby-based scss-lint in a Node project, without each developer installing that language. So they built a package manager for hooks: list the hooks you want, and pre-commit installs and runs them in any language, without root. The first release, 0.1.0, came out in June 2014, and Yelp announced it publicly that August.

The project kept the same model while growing its language list and its scope beyond the pre-commit hook. Version 1.0.0 (September 2017) adopted semantic versioning, moved the cache to ~/.cache/pre-commit and introduced the repos: config format still used today. A hosted companion, pre-commit.ci, now runs the config on GitHub pull requests and pushes the fixes. Recent releases follow Git itself: Git 2.54 added hooks configured through git config, and pre-commit 4.6.0 (April 2026) added support for installing that way. The latest release is 4.7.0, from October 12, 2026.

Why it’s built this way

Install each hook into its own environment, in the hook’s language
Yelp wanted to use the best linter for each language without forcing projects to add a Gemfile or developers to install Ruby or Node first. Managing the environments itself, without root, made any hook usable in any project.
Pin hooks to a Git repository and revision
Copy-pasted bash scripts drifted between projects. Referring to a repository and tag means every project and developer runs exactly the same version, and updating is a one-line change that autoupdate can make.
Run on staged contents only
Checking the working tree would give false positives and false negatives for what is actually being committed, so pre-commit stashes unstaged changes for the duration of the run.

Timeline

  1. 2014

    First commit March 13 by Anthony Sottile and Ken Struys; 0.1.0 released June 7; Yelp announces it August 27

  2. 2017

    1.0.0 (September 7): semantic versioning, the repos: config format, cache moved to ~/.cache/pre-commit

  3. 2020

    2.0.0 (January 28)

  4. 2023

    3.0.0 (January 23) drops Python older than 3.8; 3.2.0 (March 17) renames the commit and push stages to match the hook names

  5. 2024

    4.0.0 (October 5) warns about the old stage names

  6. 2026

    4.6.0 (April 21) supports Git 2.54’s config-based hooks; 4.7.0 released October 12

Sources12 · facts checked 13 October 2026

Improve this page on GitHub