Set Up Pre-Commit Hooks
How to set up pre-commit hooks with husky, lint-staged, and pre-commit to enforce code quality before commits
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Pre-commit hooks automatically run checks on your code before every commit. They catch linting errors, formatting issues, failing tests, and security vulnerabilities at the earliest possible moment—before they reach CI or production. Below is a practical approach to setting up hooks with the pre-commit framework (Python), husky + lint-staged (JavaScript), and native Git hooks for Java projects.
When to Use
Use this resource when:
- Your team repeatedly commits code that fails CI lint or format checks. See GitHub Actions for CI pipeline setup.
- You want to enforce code style without relying solely on PR reviews. See Unit Testing for automated quality gates.
- You need to run secrets scanning or vulnerability checks on every commit. See Container Security Scanning for security scanning.
- You want fast feedback: fix issues locally instead of waiting for CI to fail. See Bash Scripting Automation for local automation scripts.
Solution
Python
# Install pre-commit framework
# pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
language_version: python3.11
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
args: ['--max-line-length=100']
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.1
hooks:
- id: mypy
# Install hooks into .git/hooks/
# pre-commit install
# Run manually on all files
# pre-commit run --all-files
JavaScript
// package.json scripts + husky + lint-staged
// npm install --save-dev husky lint-staged prettier eslint
// package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml}": ["prettier --write"]
},
"scripts": {
"prepare": "husky install",
"lint": "eslint .",
"format": "prettier --write ."
}
}
// .husky/pre-commit (generated by npx husky add .husky/pre-commit)
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
// Or with the newer husky v9+ syntax:
// echo "npx lint-staged" > .husky/pre-commit
// .lintstagedrc.js
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'*.{json,md,yaml}': ['prettier --write'],
};
Java
// Java projects typically use Maven or Gradle hooks, not husky.
// Option 1: Maven git hook plugin (com.rudikershaw.gitbuildhook)
// pom.xml:
/*
<plugin>
<groupId>com.rudikershaw.gitbuildhook</groupId>
<artifactId>git-build-hook-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<installHooks>${project.basedir}/git-hooks</installHooks>
</configuration>
</plugin>
*/
// Option 2: Gradle + Spotless + custom Git hook
// build.gradle:
plugins {
id 'com.diffplug.spotless' version '6.23.0'
}
spotless {
java {
googleJavaFormat()
}
}
// git-hooks/pre-commit (chmod +x)
#!/bin/sh
./gradlew spotlessCheck
if [ $? -ne 0 ]; then
echo "Spotless check failed. Run './gradlew spotlessApply' to fix."
exit 1
fi
Explanation
Git hooks are executable scripts in .git/hooks/ that run at specific lifecycle events. The pre-commit hook runs after git commit is invoked but before the commit is created. If the hook exits with a non-zero status, the commit is aborted.
How the tools work:
- pre-commit (Python framework): Manages hook installation and execution across languages. Defined in
.pre-commit-config.yaml. - husky + lint-staged: Husky installs the Git hook; lint-staged filters file paths so only staged files are checked, making commits fast.
- Native Git hooks: Any executable script works. Use Maven/Gradle plugins to distribute hooks across the team.
Trade-offs:
- Hooks add commit latency (seconds to tens of seconds)
- Team members can bypass hooks with
git commit --no-verify - Hooks must be installed per clone; CI is still the ultimate gate
Variants
| Technology | Tooling | Notes |
|---|---|---|
| Python | pre-commit framework | Mature ecosystem; 200+ community hooks available |
| JavaScript / TypeScript | husky + lint-staged | Industry standard for Node.js; fast because only staged files are checked |
| Java | Maven git-build-hook-plugin or Gradle spotless | Run formatters as part of build; hooks call ./gradlew spotlessCheck |
| Go | pre-commit + golangci-lint | Use the pre-commit framework with Go-specific hooks |
| Rust | pre-commit + rustfmt / clippy | Same framework; community hooks available |
| Secrets scanning | gitleaks, trufflehog | Pre-commit hooks prevent API keys and passwords from entering history |
What Works
- Keep hooks fast: lint only staged files, not the entire codebase
- Auto-fix when possible: formatters should rewrite files, not just report errors
- Include a
prepareorpostinstallscript so hooks are auto-installed onnpm installorpip install - Run the same checks in CI; hooks are a convenience, not a replacement for CI gates
- Document bypass procedures (
--no-verify) for emergencies, but require PR review when used
Common Mistakes
- Checking the entire repo on every commit — lint-staged and pre-commit’s
filesfilter ensure only changed files are checked - Not auto-installing hooks — new clones skip hooks unless a
preparescript installs them - Conflicting formatters — ensure Prettier and ESLint rules agree; use
eslint-config-prettierto disable conflicting ESLint format rules - Hooks that modify files but don’t re-stage — if a hook reformats code, it must add the file back to the index or the commit will use the old version
- Relying only on hooks — developers can use
--no-verify; CI must enforce the same rules
Frequently Asked Questions
Can I skip hooks for a specific commit?
Yes: git commit --no-verify (or -n). Use this sparingly and always follow up with a cleanup commit. Some teams require manager approval for --no-verify usage.
Should I run tests in pre-commit hooks?
Unit tests: sometimes, if they complete in under 10 seconds. Integration or E2E tests: never; they belong in CI. Slow hooks train developers to bypass them.
How do I share hooks across my team?
Use pre-commit (cross-language) or husky (Node.js). Both store hook configuration in the repo. For Java, use a Maven/Gradle plugin that installs hooks from a tracked git-hooks/ directory during build. Never commit files directly to .git/hooks/—that directory is not tracked.
Go with golangci-lint
# .pre-commit-config.yaml
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v1.55.2
hooks:
- id: golangci-lint
args: [--config, .golangci.yml]
# .golangci.yml
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
linters-settings:
errcheck:
check-type-assertions: true
Secrets Scanning with gitleaks
# .pre-commit-config.yaml — add gitleaks
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.1
hooks:
- id: gitleaks
# Run gitleaks manually
$ gitleaks detect --source . --verbose
# Scan a specific commit range
$ gitleaks detect --source . --log-opts="HEAD~1..HEAD"
# Custom rules in .gitleaks.toml
[[rules]]
id = "custom-api-key"
description = "Custom API key pattern"
regex = '''sk-live-[a-zA-Z0-9]{20,}'''
tags = ["api-key", "custom"]
Commit Message Linting with commitlint
// Install: npm install --save-dev @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
["feat", "fix", "docs", "style", "refactor", "test", "chore", "ci", "perf"],
],
"subject-max-length": [2, "always", 72],
"body-max-line-length": [2, "always", 100],
},
};
# .husky/commit-msg
#!/bin/sh
npx --no-install commitlint --edit "$1"
# Valid commit messages:
feat: add user registration endpoint
fix: handle null pointer in auth middleware
docs: update API documentation for v2
chore: upgrade dependencies to latest patch
CI Integration (GitHub Actions)
# .github/workflows/lint.yml
name: Lint and Format
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.11"
- uses: pre-commit/action@v3.0.0
with:
extra_args: --all-files
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for scanning
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Pre-commit Hook for Type Checking
// package.json — add type-check to lint-staged
{
"lint-staged": {
"*.{ts,tsx}": ["tsc --noEmit", "eslint --fix", "prettier --write"],
"*.{js,jsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml}": ["prettier --write"]
}
}
# .pre-commit-config.yaml — Python type checking
repos:
- repo: local
hooks:
- id: mypy
name: mypy type check
entry: mypy
language: system
types: [python]
pass_filenames: false
args: [--strict, src/]
Additional Best Practices
- Use
--no-verifysparingly. Track bypass usage in commit messages:
# Document why hooks were bypassed
$ git commit --no-verify -m "fix: hotfix for prod outage (bypass: time-critical)"
- Cache pre-commit environments. Speed up hook execution by caching:
# .github/workflows/lint.yml
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- Run hooks in Docker for consistency. Ensure all team members use the same tool versions:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: docker-lint
name: ESLint in Docker
entry: docker run --rm -v $(pwd):/app node:20-slim sh -c "cd /app && npx eslint --fix"
language: system
types: [javascript]
Additional Common Mistakes
- Not pinning hook versions. Floating
rev: maincauses breakage when upstream changes:
# Bad: floating revision
repos:
- repo: https://github.com/psf/black
rev: main # Will break without warning
# Good: pinned version
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
- Hooks that require network access. Hooks should work offline. If a hook needs network (e.g., downloading rules), cache them:
# Pre-download rules during setup
$ pre-commit run --all-files # Downloads and caches on first run
Additional FAQ
How do I skip specific hooks temporarily?
With pre-commit, skip individual hooks using SKIP:
$ SKIP=flake8,mypy git commit -m "wip"
With husky, modify the hook script to check for an env var:
# .husky/pre-commit
if [ "$SKIP_HOOKS" = "1" ]; then
exit 0
fi
npx lint-staged
Can I run hooks only on specific branches?
Yes. Add a branch check at the top of your hook script:
#!/bin/sh
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ] || [ "$branch" = "release/"* ]; then
npx lint-staged
fi
How do I debug a failing pre-commit hook?
Run hooks manually with verbose output:
# Run specific hook with verbose
$ pre-commit run black --verbose --all-files
# Run with trace output
$ pre-commit run --all-files --verbose 2>&1 | tee pre-commit.log
# Check hook environment
$ pre-commit clean
$ pre-commit install
Performance Tips
- Only check staged files. Use
lint-stagedorpre-commit’sfilesfilter:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
files: ^src/.*\.py$ # Only check src/ directory
- Parallelize hooks.
pre-commitruns hooks in parallel by default. Keep them independent:
# Each repo runs in its own environment — parallel by default
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
- Use local hooks for speed. Avoid downloading remote repos for simple checks:
repos:
- repo: local
hooks:
- id: local-format
name: format check
entry: ./scripts/format.sh
language: system
files: \.(js|ts)$
- Cache tool installations. Use
language_versionto pin and cache:
hooks:
- id: black
language_version: python3.11 # Cached after first run
- Skip hooks for generated files. Exclude auto-generated code from checks:
hooks:
- id: flake8
exclude: |
(?x)^(
src/generated/.*|
migrations/.*
)$ Related Resources
Bug Report Template
A structured bug report template to help teams reproduce, triage, and resolve defects faster with clear reproduction steps and expected behavior.
DocChangelog Template
A structured changelog template following Keep a Changelog conventions for tracking project releases.
DocCode of Conduct Template
A community code of conduct template to establish inclusive, respectful collaboration standards.
DocContributing Guide Template
A ready-to-use template for open-source and internal project contribution guidelines.
DocDisaster Recovery Plan Template
A disaster recovery plan template for documenting RTO/RPO targets, failover procedures, and recovery runbooks that minimize downtime during catastrophic failures.
RecipeBash Scripting for DevOps Automation and System Tasks
How to write reliable Bash scripts for automating deployments, system monitoring, log rotation, and routine maintenance tasks