Pre-Commit Hooks Framework for Automated Code Quality
The Config First
Here's the .pre-commit-config.yaml that catches 90% of issues before they hit CI. Then I'll walk through it.
# .pre-commit-config.yaml
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
args: [--allow-multiple-documents]
- id: check-json
- id: check-merge-conflict
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main, --branch, master]
- repo: https://github.com/psf/black
rev: "24.4.2"
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: [--fix]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.92.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
That runs on every git commit. No exceptions. No excuses.
Why Pre-Commit Hooks
Code reviews should be about logic and architecture. Not trailing whitespace. Not import ordering. Not formatting debates.
Pre-commit hooks eliminate the noise:
- Catch issues in seconds, not in a 15-minute CI run
- Fix problems automatically — Black formats, Ruff fixes lint errors
- Block bad commits before they pollute your history
- Standardize across the team without relying on editor configs
If it's not automated, it doesn't exist. Manual "please run the linter" comments in PRs are proof.
Installation
# Install pre-commit
pip install pre-commit
# Or via Homebrew
brew install pre-commit
# Install the hooks in your repo
cd your-repo
pre-commit install
# Also install for commit messages (optional)
pre-commit install --hook-type commit-msg
That last command is a one-time setup per clone. After this, hooks run automatically.
Running Against All Files
First time setting up hooks? Run them against everything:
# Check all files, not just staged ones
pre-commit run --all-files
# Run a specific hook
pre-commit run black --all-files
# Run only on staged files (what happens on commit)
pre-commit run
Expect failures on the first run. That's the point. Fix them all now so every future commit starts clean.
Breaking Down the Hooks
The Foundation Hooks
- 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-merge-conflict
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main]
These are non-negotiable. detect-private-key has saved teams from credential leaks. no-commit-to-branch prevents accidental pushes to main. Zero configuration, maximum protection.
Auto-Formatting
- repo: https://github.com/psf/black
rev: "24.4.2"
hooks:
- id: black
Black formats your Python code. No arguments. No debates. The hook modifies files in-place, then the commit is aborted so you can review the changes and re-stage.
Fast Linting with Ruff
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: [--fix]
Ruff replaces Flake8, isort, and a dozen other tools. It runs in milliseconds, not seconds. The --fix flag auto-corrects safe issues.
Custom Local Hooks
Need something specific to your project? Write a local hook:
- repo: local
hooks:
- id: check-migrations
name: Check Django migrations
entry: python manage.py makemigrations --check --dry-run
language: system
types: [python]
pass_filenames: false
- id: helm-lint
name: Lint Helm charts
entry: helm lint
language: system
files: ^charts/
pass_filenames: false
Local hooks don't need an external repo. They run any command available on the system.
CI Integration
Pre-commit should also run in CI as a safety net:
# .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
Five lines. Catches anyone who skipped local hooks with --no-verify. Trust but verify.
Keeping Hooks Updated
# Update all hooks to latest versions
pre-commit autoupdate
# Update a specific repo
pre-commit autoupdate --repo https://github.com/psf/black
# Freeze to exact SHAs (for reproducibility)
pre-commit autoupdate --freeze
Run pre-commit autoupdate monthly. Pin to tags, not branches. Use --freeze in security-sensitive environments to get SHA-pinned versions.
Performance Tips
Slow hooks kill adoption. Keep them fast:
# Only run on relevant files
- id: mypy
files: ^src/
types: [python]
# Skip heavy hooks during rapid iteration
# Developers can run: SKIP=mypy git commit -m "wip"
Target file types with types and files patterns. MyPy doesn't need to scan your docs folder. Hadolint only cares about Dockerfiles.
Allow temporary skips with the SKIP environment variable. Better than --no-verify because it only skips specific hooks.
Language-Specific Hook Configurations
JavaScript/TypeScript Projects
repos:
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, json, yaml, css]
- repo: local
hooks:
- id: eslint
name: ESLint
entry: npx eslint --fix
language: system
types_or: [javascript, jsx, ts, tsx]
pass_filenames: true
Go Projects
repos:
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
- id: go-vet
- id: golangci-lint
args: [--fast]
- repo: local
hooks:
- id: go-test
name: Run Go unit tests
entry: go test -short ./...
language: system
pass_filenames: false
types: [go]
Multi-Language Monorepo
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: detect-private-key
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: [--fix]
files: ^(services/api|libs/python)/
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.92.0
hooks:
- id: terraform_fmt
files: ^infrastructure/
- id: terraform_validate
files: ^infrastructure/
Use files patterns to scope hooks to specific directories. In a monorepo, Python linting shouldn't slow down a developer who only changed Terraform files.
Secrets Detection Deep Dive
Credential leaks are expensive. Set up multiple layers of detection.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: detect-private-key
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
Gitleaks catches more than just private keys. It detects AWS access keys, GitHub tokens, Slack webhooks, database connection strings, and dozens of other patterns.
Create a .gitleaks.toml to customize:
# .gitleaks.toml
[allowlist]
paths = [
'''test/fixtures/.*''',
'''docs/examples/.*'''
]
[[rules]]
id = "custom-api-key"
description = "Internal API key pattern"
regex = '''DEVOPSIL_API_KEY_[A-Za-z0-9]{32}'''
secretGroup = 0
Team Adoption Strategy
The biggest risk with pre-commit hooks isn't the technology — it's adoption. Here's how to roll them out without a revolt.
Phase 1: Start with formatters only. Black, Prettier, terraform_fmt. These auto-fix issues without requiring manual work. Developers see immediate value.
Phase 2: Add linters with auto-fix. Ruff with --fix, ESLint with --fix. Still minimal friction — most issues resolve automatically.
Phase 3: Add validators. MyPy, terraform_validate, kubeval. These require manual fixes but catch real bugs.
Phase 4: Add security checks. Gitleaks, detect-private-key, Hadolint. Now the team trusts the system enough to accept blocking checks.
At each phase, run pre-commit run --all-files on the main branch first to fix existing violations. Never add a hook that immediately fails on existing code — that teaches people to skip hooks.
Troubleshooting
Problem: Hook runs but doesn't see all files.
Fix: Pre-commit only checks staged files by default. Use --all-files for full scans. If you partially stage a file (git add -p), the hook sees the staged version, not the working directory version.
Problem: Different results locally vs CI.
Fix: Pin exact versions with rev. Ensure CI uses the same .pre-commit-config.yaml. Check that system-level hooks have the same tool version in both environments.
Problem: Team members skip hooks with --no-verify.
Fix: Run pre-commit in CI. The CI check is the enforcement. Local hooks are the convenience.
Problem: Hooks are too slow (> 10 seconds).
Fix: Profile with pre-commit run --verbose to identify the slow hook. Scope heavy hooks with files patterns. Move truly slow checks (full test suites) to CI instead.
Problem: Python version conflicts between hooks and project.
Fix: Pre-commit manages its own virtual environments per hook. If a hook needs a specific Python version, set language_version: python3.12 on the hook entry.
Measuring Impact
Track these metrics before and after rolling out pre-commit hooks:
- CI failure rate due to lint/format issues — should drop to near zero
- Average PR review cycle time — should decrease as reviewers stop flagging style issues
- Number of PR comments about formatting — should vanish
- Secret leak incidents — should drop to zero for committed code
Run this to see how many commits would have been caught:
# Check how many files currently fail hooks
pre-commit run --all-files 2>&1 | grep -c "Failed"
If the number is high on first run, that's technical debt being surfaced. Fix it once, and every future commit starts clean.
Conclusion
Pre-commit hooks shift quality checks left — to the moment code is written, not the moment it's reviewed. Install the framework, add the hooks, run them in CI as backup. Start with formatters, add linters, then layer in security scanning. Scope hooks to relevant files in monorepos. Roll out gradually so the team builds trust in the system. Arguments about code style end permanently. Your reviewers focus on what matters. That's the whole point.
Related Articles
CI/CD Engineering Lead
Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.
Related Articles
Git Commands: Cheat Sheet
Git commands cheat sheet for DevOps engineers — branching, rebasing, stashing, bisecting, cherry-picking, and recovery workflows with examples.
The Complete Guide to GitHub Actions CI/CD: From Zero to Production-Ready Pipelines
Build production-grade GitHub Actions CI/CD pipelines — from first workflow to reusable workflows, matrix builds, and deployment gates.
Automated Semantic Versioning with Conventional Commits and release-please
Stop manually bumping versions. Use conventional commits and release-please to automate versioning, changelogs, and releases.