Skip to content
Commitpedia
Log in Join & edit

pre-commit hooks: catch mistakes before they are committed

Install the pre-commit framework, configure hooks for whitespace, YAML and JSON syntax, large files, leaked private keys, Python linting and shellcheck, run them on every commit and in CI, and keep them updated.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of pre-commit hooks: catch mistakes before they are committed: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
4 min

Git can run a script before each commit; the pre-commit framework makes those scripts shareable. One YAML file in the repository lists the checks, every developer runs pre-commit install once, and from then on trailing whitespace, broken YAML, an accidentally staged private key or a formatting slip is caught on the laptop in a second instead of in CI ten minutes later. The same configuration runs in CI as a safety net for people who skipped the hook.

Step 1: install the tool

pipx install pre-commit
pre-commit --version

Alternatives: brew install pre-commit on macOS, pip install pre-commit inside a project virtual environment, or your distribution’s package. It needs Python to run itself, but the hooks it manages can be written in any language; pre-commit installs each hook’s toolchain into an isolated environment under ~/.cache/pre-commit.

Step 2: the configuration file

In the root of your repository create .pre-commit-config.yaml:

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-json
      - id: check-merge-conflict
      - id: check-added-large-files
        args: ["--maxkb=1000"]
      - id: detect-private-key

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

  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.10.0.1
    hooks:
      - id: shellcheck
  • Each repo is a Git repository that publishes hooks; rev pins the exact tag so every developer runs the same version.
  • pre-commit-hooks is the standard collection: whitespace and newline fixes, syntax checks for YAML and JSON, a guard against committing merge conflict markers, a size limit for new files, and detect-private-key, which blocks files containing BEGIN ... PRIVATE KEY.
  • ruff lints and formats Python and fixes what it safely can; for other languages swap in the matching hook, for example pre-commit/mirrors-eslint for JavaScript or golangci/golangci-lint for Go.
  • shellcheck catches quoting and portability bugs in shell scripts.

The pinned revisions above will be behind by the time you read this. Run pre-commit autoupdate right after creating the file and it rewrites every rev to the newest tag.

Step 3: install the hook and run it on everything

pre-commit install
pre-commit run --all-files

install writes a small script to .git/hooks/pre-commit; the first run downloads the hook repositories, which takes a minute, and then checks every tracked file. Expect a wall of fixes on an older project: hooks like trailing-whitespace modify files and report Failed so you can review the changes with git diff, then stage and commit them as one cleanup commit.

Step 4: what a commit looks like now

git add .
git commit -m "Add user export"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check json...........................................(no files to check)Skipped
check for merge conflicts................................................Passed
check for added large files..............................................Passed
detect private key.......................................................Passed
ruff.....................................................................Failed
- hook id: ruff
- exit code: 1

app/export.py:12:8: F401 [*] `os` imported but unused

The commit is aborted. Fix the file (or let the hook fix it, if it can), git add it again and repeat the commit. Hooks only see the files that are staged, so a commit touching two files takes well under a second.

Skipping a hook when you must

SKIP=ruff git commit -m "WIP: refactor in progress"
git commit --no-verify -m "Emergency hotfix, lint follows"

SKIP disables named hooks for one commit; --no-verify disables all of them. Both are legitimate for a work-in-progress commit on a private branch. The CI job in the next step is what makes sure the skipped check is still applied before the code reaches main.

Step 5: run the same checks in CI

A GitHub Actions workflow that runs every hook on every file:

name: pre-commit

on:
  pull_request:
  push:
    branches: [main]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: python -m pip install pre-commit
      - run: pre-commit run --all-files --show-diff-on-failure

--show-diff-on-failure prints what the fixing hooks would have changed, so the author can see the problem from the CI log. For GitHub repositories the hosted service pre-commit.ci does the same and can push the fixes to the pull request automatically. On GitLab, the equivalent is a job with the same two commands in .gitlab-ci.yml.

Keeping it healthy

  • pre-commit autoupdate once a month, in its own commit, so a hook upgrade that introduces new findings is easy to identify.
  • pre-commit run ruff --all-files runs a single hook; handy after changing its arguments.
  • pre-commit clean and pre-commit gc remove cached hook environments when the cache grows or a hook misbehaves.
  • Exclude generated files with a top-level exclude: ^(dist/|vendor/) regular expression rather than disabling hooks.
  • Keep the hook set fast. Full test suites belong in CI; pre-commit is for checks that finish in seconds.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0032 First-time Git setup: name, email, default branch, SSH or HTTPS Tell Git who you are, pick sensible defaults, and choose between SSH and HTTPS for GitHub. Includes a small script that applies… Beginner · Step by step · Linux, macOS, Windows Script Setup 15 Oct 2026
  • 0033 The everyday Git workflow: status, add, commit, push, pull Git moves your changes through three places: the working folder, the staging area, and the commit history. These five commands, plus .gitignore… Beginner · Explains an idea · Any system Article Housekeeping · Setup 15 Oct 2026
  • 0262 Make your first pull request: Git and GitHub from first commit to merged change A complete beginner's path through Git and GitHub: set up Git, make commits, ignore files, use branches, push to GitHub, open a… Beginner · Guided project · Any system Article Branching & rebasing · Pull requests · Setup 15 Oct 2026
  • 0313 What is CI/CD? CI/CD is the habit of automatically testing every code change and, once it passes, deploying it without manual steps. Beginner · Explains an idea · Any system Article Pipelines, runners & hooks 15 Oct 2026
  • 0318 Fix: fatal: not a git repository Git looked in the current directory and every parent for a .git folder and found none. You are in the wrong directory,… Beginner · Fixes an error · Any system Article Common errors · Setup 15 Oct 2026
  • 0378 Continuous integration explained What continuous integration means, why small frequent merges plus an automated build on every change prevent integration pain, what a good pipeline… Beginner · Explains an idea · Any system Article Pipelines, runners & hooks 15 Oct 2026

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +5,840 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?