Pre-commit hooks automatically check your code before each commit, ensuring quality and consistency.

What Are Pre-commit Hooks?

Hooks are scripts that run automatically before git commit. They:

  • Format code (Black, Prettier)
  • Sort imports (isort)
  • Check for errors (flake8, eslint)
  • Verify types (mypy)
  • Prevent common mistakes

Benefits:

  • Catch issues before CI/CD
  • Maintain consistent code style
  • Faster feedback loop
  • Reduce review comments about formatting

Installation

1. Install pre-commit

  # Using uv
uv pip install pre-commit

# Verify installation
pre-commit --version
  

2. Copy Configuration

Copy .pre-commit-config.yaml from templates:

  cp docs/04-templates/.pre-commit-config.yaml .pre-commit-config.yaml
  

Or create manually - see ../04-templates/.pre-commit-config.yaml.

3. Install Hooks

  # Install hooks in your repo
pre-commit install

# Verify
ls .git/hooks/pre-commit
# Should exist
  

4. Run on All Files (First Time)

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

# This will:
# - Download hook environments
# - Format all existing files
# - Report any issues

# Fix any reported issues, then commit the fixes
git add .
git commit -m "chore: setup pre-commit hooks"
  

What Hooks Run?

Python Hooks

  1. Ruff - Linter & Formatter

    • An extremely fast Python linter and code formatter, written in Rust.
    • Replaces Black, isort, flake8, and more.
    • Formats code, sorts imports, and checks for errors all in one tool.
    • Runs automatically and fixes most issues.
  2. ty - Type checker (by Astral)

    • Blazing-fast static type checker written in Rust
    • Replaces mypy with dramatically faster performance
    • Checks type annotations and catches type errors
    • Reports issues that must be fixed manually
  3. bandit - Security linter

    • Finds security issues
    • Reports vulnerabilities
    • Must fix manually
  4. interrogate - Docstring coverage

    • Checks for missing docstrings
    • Minimum 80% coverage required
    • Add docstrings to pass

JavaScript/TypeScript Hooks

  1. Biome - All-in-one toolchain (Rust-based)
    • Formats and lints JS/TS/JSON in a single tool
    • Replaces both ESLint and Prettier
    • 25x faster than ESLint + Prettier combined
    • Auto-fixes most issues
    • Near-zero configuration needed

General Hooks

  1. trailing-whitespace - Removes trailing spaces
  2. end-of-file-fixer - Ensures file ends with newline
  3. check-yaml - Validates YAML syntax
  4. check-json - Validates JSON syntax
  5. check-merge-conflict - Prevents committing merge conflicts
  6. detect-secrets - Prevents committing secrets

Daily Usage

Normal Workflow

  # 1. Make changes
vim app/main.py

# 2. Stage changes
git add app/main.py

# 3. Commit
git commit -m "feat: add new endpoint"

# Hooks run automatically!
# - Ruff formats your code and sorts imports
# - ty checks type annotations
# - Biome formats and lints JS/TS
# - etc.

# If all pass:
[main abc1234] feat: add new endpoint

# If any fail:
# - See error output
# - Fix issues
# - Try commit again
  

When Hooks Fail

Example failure:

  $ git commit -m "feat: add endpoint"

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

reformatted app/main.py
1 file reformatted, 2 imports sorted.

ty......................................................................Failed
- hook id: ty
- exit code: 1

app/main.py:10:5: error: Missing return type annotation
app/main.py:15:10: error: Argument has incompatible type "str"; expected "int"
  

Fix:

  # 1. Ruff auto-formatted the file, stage it
git add app/main.py

# 2. Fix ty type errors (add type annotations)
vim app/main.py  # Add return type and fix argument type

# 3. Stage and commit
git add app/main.py
git commit -m "feat: add endpoint"
# Should pass now!
  

Running Hooks Manually

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

# Run all hooks on staged files
pre-commit run

# Run specific hook on all files
pre-commit run ruff --all-files
pre-commit run ty --all-files
pre-commit run biome --all-files

# Run specific hook on specific file
pre-commit run ruff --files app/main.py
pre-commit run ty --files app/main.py
  

Skipping Hooks (Emergency Only)

  # Skip ALL hooks (NOT RECOMMENDED)
git commit --no-verify

# Skip specific hook
SKIP=ruff git commit -m "message"

# Skip multiple hooks
SKIP=ruff,ty git commit -m "message"
  

When to skip:

  • ⚠️ Emergency hotfix (very rare)
  • ⚠️ Fixing hook configuration itself
  • ❌ Never skip just to avoid fixing issues

Configuration

Python: pyproject.toml

  # pyproject.toml

[tool.ruff]
line-length = 88
target-version = "py311"
exclude = [
    ".git",
    ".venv",
    "__pycache__",
    "migrations",
    "build",
    "dist",
]

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "UP",  # pyupgrade
]
ignore = ["E501"]  # line too long (handled by formatter)

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.ty]
# ty uses smart defaults, minimal config needed
strict = true

[tool.bandit]
exclude_dirs = ["tests", ".venv", "migrations"]
  

Python: .flake8 (DEPRECATED - now using Ruff)

Ruff replaces flake8, so .flake8 is no longer needed. All configuration is in pyproject.toml under [tool.ruff].

JavaScript/TypeScript: biome.json

Biome replaces both ESLint and Prettier with a single configuration file:

  {
  "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "warn"
      },
      "style": {
        "useConst": "error",
        "noUnusedTemplateLiteral": "warn"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100,
    "lineEnding": "lf"
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "trailingCommas": "es5",
      "semicolons": "always",
      "arrowParentheses": "asNeeded"
    }
  },
  "json": {
    "formatter": {
      "enabled": true
    }
  }
}
  

Legacy Files (DEPRECATED)

.eslintrc.js and .prettierrc are no longer needed - Biome replaces both. You can safely delete these files after migrating to Biome.


Updating Hooks

  # Update all hooks to latest versions
pre-commit autoupdate

# This updates the 'rev' field in .pre-commit-config.yaml
# Review changes and commit:
git add .pre-commit-config.yaml
git commit -m "chore: update pre-commit hooks"
  

Troubleshooting

Issue: Hooks Won’t Install

  # Uninstall and reinstall
pre-commit uninstall
pre-commit install

# Clear cache
pre-commit clean

# Try again
pre-commit run --all-files
  

Issue: Hooks Very Slow

  # Hooks download dependencies first time
# Be patient, subsequent runs are faster

# If still slow:
# - Exclude large directories in .pre-commit-config.yaml
# - Use --files to run on specific files during development
  

Issue: Hook Conflicts with Editor

Editor may auto-format differently than hooks.

Solution: Configure editor to use same tools:

  • VS Code: Use Ruff extension, Biome extension
  • Match settings to .pre-commit-config.yaml
  • See editor-setup.md

Issue: ty Type Checking Errors

  # ty is stricter than mypy - this is good!
# Fix type hints in your code:

# Before:
def get_user(id):  # Missing type hints
    return db.query(User).filter(User.id == id).first()

# After:
def get_user(id: int) -> Optional[User]:
    return db.query(User).filter(User.id == id).first()

# If you need to ignore specific errors temporarily:
# Add inline comment:
# type: ignore[error-code]
  

Best Practices

Do:

  • Run hooks before pushing
  • Fix issues immediately (they’re usually quick)
  • Update hooks monthly
  • Commit hook config changes with team

Don’t:

  • Skip hooks to avoid fixing issues
  • Commit without running hooks
  • Ignore hook warnings
  • Have different configs per developer

Integration with CI/CD

Pre-commit hooks also run in CI/CD:

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

on: [push, pull_request]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - uses: pre-commit/action@v3.0.0
  

This ensures all PRs pass hooks before merging.


Common Issues and Fixes

IssueFix
Ruff changes my codeThat’s expected - it formats to standard
Ruff: line too longBreak line or refactor to be simpler
Ruff: unused variableRemove it or rename to _unused
ty: missing typeAdd type hint: def func(x: int) -> str:
Missing docstringAdd Google-style docstring
Trailing whitespaceLet hook fix it automatically
Biome formattingThat’s expected - consistent JS/TS formatting

Next Steps