Running pre-commit in CI on changed files Jump to heading

The usual CI recipe for the pre-commit framework is pre-commit run --all-files. On a small repository that is fine. On a large one it is slow, and on a repository adopting new hooks it is worse than slow: every pull request fails on files it never touched, because the new hooks flag years of existing code. Running hooks only on the files a pull request changed mirrors what developers’ local hooks do, keeps CI fast, and makes failures relevant to the change. The framework supports this directly with --from-ref and --to-ref. There is one case where checking only changed files is wrong — when the hook configuration itself changed — and the CI job should detect that and check everything. This page builds that job, within the pre-commit framework for polyglot repos.

When to use this approach Jump to heading

  • pre-commit run --all-files in CI takes minutes on your repository.
  • You are introducing hooks to a codebase with existing violations and cannot fix everything at once.
  • You want CI to match what local hooks check: the files being changed.
  • Your hooks are pinned, as in pinning and autoupdating pre-commit hook versions, so local and CI runs agree.

Step 1 — Fetch enough history for a diff Jump to heading

Checking changed files needs the merge base between the pull request and its target branch. Fetch full history, or at least enough to reach it.

      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
# Verification: the merge base is available
git merge-base "origin/$GITHUB_BASE_REF" HEAD

Step 2 — Run hooks on the pull request’s changed files Jump to heading

--from-ref and --to-ref make pre-commit run hooks on files changed between two revisions, the same set a developer’s hooks would have seen across all their commits.

      - name: pre-commit on changed files
        run: |
          base=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
          pre-commit run --show-diff-on-failure --color=always --from-ref "$base" --to-ref HEAD
All files against changed files in CIRunning every hook on every file is simple and catches everything, but is slow on large repositories and fails pull requests on old code they never touched. Running on changed files is fast and only reports problems the pull request introduced or touched.--all-files--from-ref / --to-reftime on a large repominutessecondsreports old violationsyesonly in touched filesmatches local hooksnoyescatches config changesyesneeds a fallbackthe fallback in step 3 closes the one gap changed-files mode has

Using the merge base rather than the target branch’s tip matters: comparing against the moving tip would include files changed on main since the branch was created, which are not this pull request’s concern.

Step 3 — Check everything when the hook config changes Jump to heading

If a pull request changes .pre-commit-config.yaml — adding a hook, updating versions — checking only changed files would test the new configuration against almost nothing. Detect that case and run on all files.

      - name: pre-commit
        run: |
          base=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
          if git diff --name-only "$base" HEAD | grep -qx '.pre-commit-config.yaml'; then
            echo "Hook config changed: checking all files"
            pre-commit run --show-diff-on-failure --all-files
          else
            pre-commit run --show-diff-on-failure --from-ref "$base" --to-ref HEAD
          fi
Which files should CI check?If the pull request changes the hook configuration, every file must be checked against the new rules. If the target is the default branch after a merge, a periodic all-files run catches drift. Otherwise, checking the pull request's changed files is enough.What is CI running for?config changed in PRAll filesnew rules, whole reponormal PRChanged filesmerge base to HEADnightly on mainAll filescatch driftthree modes, one script — decided by what changed

Step 4 — Run a periodic all-files check on main Jump to heading

Changed-files mode never revisits untouched files, so drift can accumulate: a file excluded from a hook by mistake, a merge that slipped through while the config was being changed. A scheduled all-files run on the default branch catches it without slowing down pull requests.

on:
  schedule: [{ cron: "11 3 * * *" }]
jobs:
  pre-commit-all:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pipx install pre-commit && pre-commit run --all-files --show-diff-on-failure

Step 5 — Adopt a new hook without a big-bang fix Jump to heading

To introduce a strict new hook into a codebase with many existing violations, changed-files mode lets you enforce it on new and touched code immediately while the backlog is fixed gradually.

# Measure the backlog once
pre-commit run new-hook --all-files 2>&1 | grep -c '^[^ ]*:[0-9]*:' || true
# Enforce on changed files from today; fix the rest in batches by directory
pre-commit run new-hook --files $(git ls-files 'services/billing/*.py')
Violations of a new hook over a quarterAn illustrative adoption of a new lint hook in changed-files mode. New violations stop immediately because touched files must pass. The backlog shrinks as files are edited for other reasons, plus a few deliberate cleanup batches, until a full run is clean.remaining violations in untouched files (illustrative)week 0640month 1410month 2180month 30once the backlog reaches zero, the nightly all-files run keeps it there

Step 6 — Keep the job fast with caching Jump to heading

Hook environments are installed on first use. Cache ~/.cache/pre-commit keyed on the config file’s hash, so CI reinstalls only when hook versions change.

      - uses: actions/cache@v4
        with:
          path: ~/.cache/pre-commit
          key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}

The caching details, including what to do for hooks with large environments, are in caching pre-commit environments in CI.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why not just pass --files with a list from git diff? Jump to heading

You can, but --from-ref/--to-ref handles renames, deletions and file name quoting for you, and matches exactly how pre-commit’s own pre-push stage selects files.

What about hooks that need to see the whole repository, like a dependency check? Jump to heading

Mark them with pass_filenames: false and always_run: true in the config. They run once regardless of which files changed, in both modes.

Does this work on GitLab? Jump to heading

Yes. Use CI_MERGE_REQUEST_DIFF_BASE_SHA as the from-ref in merge request pipelines, and the same config-change fallback.