pre-commit
pre-commit is a framework that runs linters and formatters on staged files before each git commit, using hooks pinned in a .pre-commit-config.yaml at the repo root. It catches problems locally, and the same config can run in CI. Hooks here cover shell (ShellCheck and shfmt), Markdown, YAML and secrets. Git basics are in Git.
Documentation: pre-commit.com, supported hooks list
Install and Use #
| COMMAND | DESCRIPTION |
|---|---|
uv tool install pre-commit | Install (or pipx install pre-commit, pip install pre-commit, brew install pre-commit) |
pre-commit --version | Verify |
pre-commit install | Install the git hook into .git/hooks/pre-commit |
pre-commit install --hook-type commit-msg | Also install a hook for commit messages |
pre-commit run | Run hooks on staged files |
pre-commit run --all-files | Run on every file (do this first after adding hooks) |
pre-commit run <HOOK_ID> | Run one hook |
pre-commit run <HOOK_ID> --all-files | Run one hook everywhere |
pre-commit run --files <FILE>... | Run on specific files |
pre-commit run --from-ref origin/master --to-ref HEAD | Run on files changed in a branch (CI) |
pre-commit autoupdate | Bump every rev to the latest tag |
pre-commit autoupdate --repo <REPO_URL> | Bump one repo |
pre-commit autoupdate --freeze | Pin to commit SHAs instead of tags |
pre-commit clean | Delete cached hook environments |
pre-commit gc | Remove unused cached repos |
pre-commit uninstall | Remove the git hook |
git commit --no-verify | Skip hooks for one commit (emergencies only) |
SKIP=<HOOK_ID> git commit -m "msg" | Skip specific hooks, comma-separated |
When a hook modifies files, the commit is aborted. Review the changes, git add them and commit again.
Config Reference #
# .pre-commit-config.yaml
default_install_hook_types: [pre-commit, commit-msg]
default_stages: [pre-commit]
fail_fast: false
exclude: ^(vendor/|\.venv/)
repos:
- repo: <REPO_URL>
rev: <TAG_OR_SHA>
hooks:
- id: <HOOK_ID>
args: [--option]
files: \.sh$
exclude: ^legacy/
types: [shell]
additional_dependencies: []| KEY | DESCRIPTION |
|---|---|
repo | Git repository containing the hook (or local / meta) |
rev | Tag or commit SHA to use; pin it, never use a branch name |
id | Which hook from the repository |
args | Extra command-line arguments |
files / exclude | Regex filters on file paths |
types / types_or | Filter by file type (python, shell, yaml, markdown) |
stages | pre-commit, pre-push, commit-msg, manual |
pass_filenames | Set false for hooks that scan the whole repo |
always_run | Run even when no matching file changed |
verbose | Print hook output even on success |
Example Config #
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
args: [--allow-multiple-documents]
- id: check-merge-conflict
- id: check-added-large-files
- id: detect-private-key
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
args: [-x]
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.14.1-1
hooks:
- id: shfmt
args: [-i, "2", -ci]
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.50.0
hooks:
- id: markdownlint
args: [--fix]
- repo: https://github.com/adrienverge/yamllint
rev: v1.38.0
hooks:
- id: yamllint
args: [-c, .yamllint.yml]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaksThe rev values above are examples from the time of writing. Run pre-commit autoupdate to move them to current releases and let Renovate keep them fresh (it supports pre-commit configs).
| HOOK | PURPOSE |
|---|---|
pre-commit-hooks | Whitespace, EOF newline, YAML/JSON syntax, merge-conflict markers, large files, private keys |
shellcheck-py | ShellCheck without a system install |
pre-commit-shfmt | shfmt without a system install |
markdownlint-cli | Markdown style (config in .markdownlint.yaml) |
yamllint | YAML style (config in .yamllint.yml) |
gitleaks | Secret scanning of staged changes |
Related config files:
# .yamllint.yml
extends: default
rules:
line-length: disable
truthy: { check-keys: false }
document-start: disable# .markdownlint.yaml
default: true
MD013: false # line length
MD033: false # inline HTML
MD041: false # first line headingLocal Hooks #
Run any command already in your repo as a hook:
- repo: local
hooks:
- id: compose-config
name: docker compose config
entry: docker compose -f compose.yml config -q
language: system
files: ^compose\.ya?ml$
pass_filenames: falselanguage: system uses whatever is on PATH; other languages (python, node, docker, script) build their own environment.
CI #
# GitHub Actions
name: pre-commit
on: [push, pull_request]
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# Woodpecker CI
steps:
- name: pre-commit
image: python:3.12
commands:
- pip install pre-commit
- pre-commit run --all-files --show-diff-on-failure--show-diff-on-failure prints what a fixing hook changed. Cache ~/.cache/pre-commit between runs to save time.
Tips #
- Run
pre-commit run --all-filesonce when adding a new hook, and commit the resulting fixes on their own. - Use
stages: [manual]for slow hooks and run them withpre-commit run --hook-stage manual. gitleaksonly finds secrets in what you commit; rotate any secret that has ever been pushed.- A hook failing in CI but not locally usually means different hook versions or a missing
pre-commit install. - See also Bash Recipes for the scripts that local hooks often call.