Pre-commit Hooks Setup Guide
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
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.
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
bandit - Security linter
- Finds security issues
- Reports vulnerabilities
- Must fix manually
interrogate - Docstring coverage
- Checks for missing docstrings
- Minimum 80% coverage required
- Add docstrings to pass
JavaScript/TypeScript Hooks
- 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
- trailing-whitespace - Removes trailing spaces
- end-of-file-fixer - Ensures file ends with newline
- check-yaml - Validates YAML syntax
- check-json - Validates JSON syntax
- check-merge-conflict - Prevents committing merge conflicts
- 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
| Issue | Fix |
|---|---|
| Ruff changes my code | That’s expected - it formats to standard |
| Ruff: line too long | Break line or refactor to be simpler |
| Ruff: unused variable | Remove it or rename to _unused |
| ty: missing type | Add type hint: def func(x: int) -> str: |
| Missing docstring | Add Google-style docstring |
| Trailing whitespace | Let hook fix it automatically |
| Biome formatting | That’s expected - consistent JS/TS formatting |
Next Steps
- Configure editor to match hooks:
editor-setup.md - Learn our code standards:
../02-standards/code-standards.md - Review commit workflow:
../03-workflows/git-workflow.md