콘텐츠로 이동

pre-commit

Installation

# pip (recommended)
pip install pre-commit

# pipx (isolated, no venv needed)
pipx install pre-commit

# Homebrew
brew install pre-commit

# conda
conda install -c conda-forge pre-commit

# Verify
pre-commit --version

Install the git hook into your repository:

# Run from repo root — installs .git/hooks/pre-commit
pre-commit install

# Also install commit-msg hook (for commitlint etc.)
pre-commit install --hook-type commit-msg

# Install pre-push hook
pre-commit install --hook-type pre-push

Configuration

Create .pre-commit-config.yaml in your repo root:

Minimal Config

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
default_language_version:
  python: python3.12

default_stages: [pre-commit]

fail_fast: false

repos:
  # General file hygiene
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
        args: [--allow-multiple-documents]
      - id: check-toml
      - id: check-json
      - id: check-merge-conflict
      - id: check-added-large-files
        args: [--maxkb=1000]
      - id: detect-private-key
      - id: no-commit-to-branch
        args: [--branch, main, --branch, master]
      - id: mixed-line-ending

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

  # Python type checking
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests, types-pyyaml]

  # Security scanning
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.8
    hooks:
      - id: bandit
        args: [-r, src/]
        exclude: tests/

  # Secret detection
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: [--baseline, .secrets.baseline]

  # Commit message linting
  - repo: https://github.com/commitizen-tools/commitizen
    rev: v3.24.0
    hooks:
      - id: commitizen
        stages: [commit-msg]

  # Markdown linting
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.40.0
    hooks:
      - id: markdownlint
        args: [--fix]

JavaScript / Node Config

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-json
      - id: detect-private-key

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v9.2.0
    hooks:
      - id: eslint
        files: \.(js|jsx|ts|tsx)$
        additional_dependencies:
          - eslint@9.2.0
          - eslint-plugin-react@7.34.1

  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v4.0.0-alpha.8
    hooks:
      - id: prettier
        files: \.(js|jsx|ts|tsx|json|css|scss|md|yaml)$

Core Commands

CommandDescription
pre-commit installInstall hooks into .git/hooks/
pre-commit uninstallRemove hooks from .git/hooks/
pre-commit runRun hooks on staged files
pre-commit run --all-filesRun hooks on all repo files
pre-commit run <hook-id>Run a specific hook only
pre-commit run --files a.py b.pyRun on specific files
pre-commit autoupdateBump all hook revisions to latest
pre-commit autoupdate --repo https://...Update a single repo
pre-commit cleanClear hook environments cache
pre-commit gcGarbage-collect unused envs
pre-commit validate-configValidate .pre-commit-config.yaml
pre-commit validate-manifestValidate .pre-commit-hooks.yaml
pre-commit sample-configPrint a starter config
pre-commit try-repo <url>Test a hook repo without installing
pre-commit try-repo <url> <hook-id>Test a specific hook

Skip and Override

CommandDescription
SKIP=flake8 git commitSkip a specific hook by ID
git commit --no-verifySkip all hooks for this commit
PRE_COMMIT_ALLOW_NO_CONFIG=1 git commitAllow commit with no config

Advanced Usage

Local Hooks (No Remote Repo Needed)

repos:
  - repo: local
    hooks:
      # Run a shell script
      - id: run-tests
        name: Run unit tests
        entry: bash -c 'pytest tests/ -x -q'
        language: system
        pass_filenames: false
        stages: [pre-push]

      # Run a Python script
      - id: check-migrations
        name: Check for missing migrations
        entry: python manage.py makemigrations --check
        language: system
        pass_filenames: false
        types: [python]

      # Run a Node script
      - id: lint-staged
        name: ESLint staged files
        entry: npx lint-staged
        language: system
        pass_filenames: false

      # Inline script
      - id: no-debug-statements
        name: No debug statements
        entry: grep -rn 'import pdb\|breakpoint()\|console\.log'
        language: system
        types: [python, javascript]
        pass_filenames: true

Language-Specific Environments

repos:
  - repo: local
    hooks:
      # Uses the repo's own virtualenv
      - id: mypy
        name: mypy type check
        entry: mypy
        language: python
        additional_dependencies: [mypy==1.10.0]
        types: [python]

      # Golang
      - id: go-vet
        name: go vet
        entry: go vet ./...
        language: golang
        pass_filenames: false

      # Docker image
      - id: hadolint
        name: Lint Dockerfile
        entry: hadolint
        language: docker_image
        types: [dockerfile]

CI Integration

Run pre-commit in GitHub Actions without installing hooks:

# .github/workflows/pre-commit.yml
name: pre-commit

on:
  pull_request:
  push:
    branches: [main]

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

Or manually in any CI:

# Install and run without git hooks
pip install pre-commit
pre-commit run --all-files

Pinning with --freeze

# Generate a frozen lockfile with exact SHAs
pre-commit autoupdate --freeze

This produces:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.6.0  # frozen: sha256:...

Caching in CI

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

exclude and files Patterns

hooks:
  - id: trailing-whitespace
    exclude: '^(docs/|\.github/)'   # regex, excludes paths
    files: '\.py$'                   # only run on Python files
    exclude_types: [markdown]        # exclude by file type
    types_or: [python, javascript]   # run on py OR js

Hook Stages

hooks:
  - id: pytest
    stages: [pre-push]      # only on push, not commit

  - id: commitizen
    stages: [commit-msg]    # runs on commit message

  - id: ruff
    stages: [pre-commit, manual]  # manual = pre-commit run <id>

Common Workflows

Bootstrap a New Repo

# 1. Install pre-commit
pip install pre-commit

# 2. Generate a starter config
pre-commit sample-config > .pre-commit-config.yaml

# 3. Edit the config with your hooks
# 4. Install the git hook
pre-commit install

# 5. Run on all files to catch existing issues
pre-commit run --all-files

# 6. Commit the config
git add .pre-commit-config.yaml
git commit -m "chore: add pre-commit hooks"

Update All Hooks

# Bump all revs to latest tags
pre-commit autoupdate

# Review the diff
git diff .pre-commit-config.yaml

# Commit
git add .pre-commit-config.yaml
git commit -m "chore: update pre-commit hooks"

Debug a Failing Hook

# Run only the failing hook verbosely
pre-commit run <hook-id> --all-files --verbose

# Try a hook from any repo without committing
pre-commit try-repo https://github.com/astral-sh/ruff-pre-commit ruff --all-files

# Inspect hook environment
pre-commit run --show-diff-on-failure ruff

Migrate from lint-staged

# lint-staged runs per staged file — replicate with:
repos:
  - repo: local
    hooks:
      - id: eslint
        name: ESLint
        entry: eslint --fix
        language: system
        types: [javascript, jsx, ts, tsx]
        # pre-commit passes only staged files by default

Tips and Best Practices

  • Commit .pre-commit-config.yaml into version control so the whole team shares the same hooks.
  • Pin rev: to a tag or SHA, never rev: main — this ensures reproducible environments.
  • Run pre-commit run --all-files after adding new hooks to fix existing issues before they block future commits.
  • Use fail_fast: true locally for faster feedback when you know a hook will fail; use false in CI to see all issues at once.
  • SKIP=hook-id git commit lets you bypass one hook without disabling all hooks with --no-verify.
  • Cache the pre-commit environment in CI (~/.cache/pre-commit) to avoid re-downloading hooks on every run.
  • pre-commit autoupdate should be a periodic maintenance task — schedule it monthly or run it in Dependabot.
  • Local hooks are great for project-specific checks (custom linters, migration checks, test runs on push) without publishing a hook repo.
  • stages: [pre-push] for slow hooks (tests, type checking) so they don’t block every commit.
  • pre-commit gc removes cached environments for hooks that are no longer in your config — run it after major config changes.
  • detect-private-key and detect-secrets are essential security hooks — include them in every project.
  • no-commit-to-branch prevents accidental direct commits to main or master — a simple but valuable guard.