Pre-commit Framework Setup Guide: Automatically Check Code Style and Lint Before Every Commit

Git tutorial - IT technology blog
Git tutorial - IT technology blog

Background: Why Our Team Started Using Pre-commit

When our team was just 4–5 people, code reviews were pretty relaxed — everyone knew each other and understood each other’s coding style. But when we grew to 8 people with 3 seniors, 2 mid-levels, and 3 new juniors, things started getting messy. Every PR had comments like “missing newline at end of file”, “tabs instead of spaces”, “imports not sorted”. Reviewers wasted time on it, and the people being reviewed got frustrated by comments about things that a tool should catch automatically.

I decided to adopt the pre-commit framework after seeing an open source repo use it. The result: style/format comments in PRs dropped to nearly zero, and reviewers could focus on logic instead of trailing commas. In our team of 8, I applied pre-commit alongside the Git flow we already had — and merge conflicts also dropped significantly because code became more consistent with fewer formatting conflicts.

Pre-commit is a framework for managing Git hooks — specifically the pre-commit hook that runs right before git commit creates a commit. If any hook fails, the commit is blocked. What sets it apart from writing hooks manually is that pre-commit manages each hook’s dependencies separately, without touching your project’s Python or Node environment — each hook runs in its own isolated virtualenv.

Installing Pre-commit

Requirements

Pre-commit requires Python 3.8+ and pip. Check first:

python --version   # Python 3.8.x or higher
pip --version

Installing Globally or in a Virtualenv

# Install globally
pip install pre-commit

# Or with pipx (isolated, cleaner — this is what I use)
pipx install pre-commit

# Check version
pre-commit --version
# pre-commit 3.7.x

Activating Hooks for Your Repo

Once installed, navigate to your repo directory and run a single command:

cd /path/to/your/repo
pre-commit install

This creates the .git/hooks/pre-commit file — from now on, every time you run git commit, pre-commit runs automatically. To also block git push, add:

pre-commit install --hook-type pre-push

Detailed Configuration with .pre-commit-config.yaml

All configuration lives in the .pre-commit-config.yaml file at the root of your repo. Since this file is committed to git, everyone on the team automatically shares the same config — no need for individual setup.

Basic Configuration for a Python Project

# .pre-commit-config.yaml
repos:
  # Basic hooks from pre-commit-hooks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace      # Remove trailing whitespace
      - id: end-of-file-fixer        # Ensure files end with a newline
      - id: check-yaml               # Validate YAML syntax
      - id: check-json               # Validate JSON syntax
      - id: check-merge-conflict     # Detect leftover conflict markers
      - id: check-added-large-files  # Block oversized files (prevent accidental binary commits)
        args: ['--maxkb=500']
      - id: debug-statements         # Detect leftover print(), breakpoint() calls

  # Black — code formatter for Python
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black
        language_version: python3

  # isort — sort import statements
  - repo: https://github.com/PyCQA/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ["--profile", "black"]  # Compatible with Black

  # Flake8 — linter for logic and style errors
  - repo: https://github.com/PyCQA/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: ['--max-line-length=88']  # Match Black's default line length

Adding Hooks for JavaScript/TypeScript

For projects mixing Python and JS (API backend + frontend), add to the repos section:

  # Prettier — format JS/TS/CSS/JSON
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v4.0.0-alpha.8
    hooks:
      - id: prettier
        types_or: [javascript, typescript, css, json]

  # ESLint
  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v9.4.0
    hooks:
      - id: eslint
        files: \.(js|ts|jsx|tsx)$
        types: [file]

Local Hooks — Writing Your Own Scripts

This is the part I find most powerful: write your own hooks without publishing them anywhere — just keep them in the repo:

  # Local hooks — run scripts directly in the repo
  - repo: local
    hooks:
      - id: no-leftover-todos
        name: Check for leftover TODO/FIXME in staged files
        entry: grep -n "TODO\|FIXME\|HACK"
        language: system
        types: [python]
        pass_filenames: true

      - id: run-unit-tests
        name: Run unit tests
        entry: python -m pytest tests/unit -q --tb=short
        language: system
        pass_filenames: false
        stages: [pre-push]  # Only runs on push, not on every commit

Note the stages: [pre-push] — heavy tests belong in pre-push, lightweight linting in pre-commit, so you don’t slow down your daily commit workflow.

Auto-updating Hook Versions

Once you’ve written your config, run this command to update the rev of all hooks to their latest versions:

pre-commit autoupdate

I schedule this to run once a month — to avoid using outdated hooks with known security issues.

Testing and Monitoring

Running Manually Before Committing

To check the entire codebase without creating a dummy commit:

# Run all hooks on all files
pre-commit run --all-files

# Run only a specific hook
pre-commit run black --all-files
pre-commit run flake8 --all-files

# Run on specific files
pre-commit run --files src/main.py src/utils.py

The first time you run it, pre-commit downloads and installs a separate environment for each hook — this takes about 1–2 minutes. After that, everything is cached and runs immediately.

Reading Output When a Hook Fails

Here’s the actual output when a commit is blocked:

$ git commit -m "Add user authentication"
[INFO] Stashing unstaged changes to tracked files.
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check for merge conflicts................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted src/auth.py

isort....................................................................Failed
- hook id: isort
- files were modified by this hook

Fixing src/auth.py

Black and isort automatically fix the files — just run git add src/auth.py and commit again. With flake8 it’s different: it reports errors but doesn’t auto-fix them, so you’ll need to fix them manually based on the output.

Skipping Hooks When You Really Need To

# Skip all hooks (emergency hotfix)
git commit --no-verify -m "hotfix: production down"

# Skip a specific hook
SKIP=flake8 git commit -m "WIP: will fix lint later"

In our team, we have an agreement: anyone who uses --no-verify must clearly state the reason in the commit message and create a ticket to fix it within 1 day. It’s not a strict rule, but it’s enough to keep everyone accountable.

Integrating with CI/CD to Ensure No One Skips It

Local hooks only run on developer machines — others can still push without installing pre-commit. Add a GitHub Actions workflow so CI blocks them too:

# .github/workflows/lint.yml
name: Lint Check
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/[email protected]  # Official action from pre-commit

This action reuses pre-commit’s cache and typically takes only 30–60 seconds in CI — much lighter than running the full test suite.

Onboarding New Team Members

After a new member clones the repo, they only need to run 2 commands:

pip install pre-commit
pre-commit install

I added these 2 commands to the Makefile setup target and the README so no one forgets. One more tip: run pre-commit run --all-files right during onboarding — so the new member can see the current state of the codebase and fix all legacy lint issues from day one, rather than letting them accumulate.

After the first month of rolling it out across our 8-person team, CI failures due to style issues dropped from ~40% to under 5%. More importantly, code reviews started focusing on architecture and logic — the things that actually require human judgment — instead of trailing commas that a tool can handle in seconds.

Share: