Server

pre-commit Hooks That Actually Help

By Prabath Thalangama· October 24, 2026· 4 min read
#git#ci-cd#tooling

Introduction

The pre-commit framework manages Git hooks in a language-agnostic way: hooks are defined in .pre-commit-config.yaml, run in isolated environments, and are shared across the team via the repo. It catches trivial problems before they reach review or CI.

Setup

pipx install pre-commit        # or: uv tool install pre-commit
pre-commit install             # installs the git hook
pre-commit install --hook-type commit-msg   # if you use commit-msg hooks

Now git commit runs the hooks on staged files. pre-commit run --all-files runs everything (do this once when adding to an existing repo).

A useful config

# .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-added-large-files      # default 500kB; blocks accidental binaries
        args: [--maxkb=1024]
      - id: check-merge-conflict
      - id: detect-private-key
      - id: mixed-line-ending
        args: [--fix=lf]

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

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.0
    hooks:
      - id: gitleaks               # secret scanning

  - repo: https://github.com/rhysd/actionlint
    rev: v1.7.4
    hooks:
      - id: actionlint             # GitHub Actions workflow linting

  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.42.0
    hooks:
      - id: markdownlint
        args: [--fix]

Add per-stack: prettier/eslint (JS), gofmt/golangci-lint (Go), terraform_fmt/terraform_validate (via antonbabenko/pre-commit-terraform), shellcheck, hadolint (Dockerfiles).

Auto-fix vs fail

Hooks that modify files (formatters, end-of-file-fixer, ruff --fix) exit non-zero when they change something — the commit is aborted, you git add the fixes and commit again. This is intentional: you review what the tool changed.

Hooks that only check (gitleaks, check-yaml) fail with a message; you fix and retry.

Run it in CI too

Local hooks can be skipped (git commit --no-verify). Enforce in CI:

# GitHub Actions
- uses: actions/setup-python@v5
- uses: pre-commit/[email protected]
# or, with caching, run `pre-commit run --all-files` yourself

Or use pre-commit.ci — a hosted service that runs the hooks on every PR and auto-commits fixes and hook version bumps.

Keeping it fast

  • pre-commit runs hooks only on staged files by default — fast.
  • Heavy hooks (type checkers, full test suites) don't belong in pre-commit; put them in CI or a pre-push hook.
  • pre-commit run --all-files in CI is the slow one; cache ~/.cache/pre-commit.
  • pre-commit autoupdate bumps rev: pins; run it periodically (or let pre-commit.ci / Renovate do it).

Verification and troubleshooting

pre-commit run --all-files
pre-commit run ruff --all-files          # one hook
pre-commit run --files path/to/file
SKIP=gitleaks git commit ...             # skip one hook for one commit
pre-commit clean && pre-commit gc        # nuke cached hook envs
  • Hook "passes locally, fails in CI" — different hook versions (CI installs from rev:, you have a stale cache), or the hook only checks staged files locally but --all-files in CI finds pre-existing issues. Run --all-files once and fix the backlog.
  • Executable not found / language errors — the hook's language: runtime (node, python, go, ruby) isn't available. pre-commit installs most itself; language: system hooks need the tool on PATH.
  • Commit blocked by a formatter that keeps changing the same file — a conflict between two formatters (e.g. ruff-format and black, or prettier and eslint --fix). Pick one per file type.
  • detect-private-key / gitleaks false positive on a test fixture — add a # pragma: allowlist secret (gitleaks) or an exclude: regex in the hook config; don't disable the hook.
  • Everyone uses --no-verify — the hooks are too slow or too noisy. Trim to fast, high-signal checks; move the rest to CI.
  • Hook not running at all — pre-commit install wasn't run in this clone, or core.hooksPath is set elsewhere. pre-commit install -f.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.