Git hooks are scripts that run automatically at specific points in the Git workflow — before commits, during pushes, after merges. They enforce code quality, run tests, check formatting, and prevent broken code from entering your repository. But managing hooks across a team is notoriously difficult: hooks live in .git/hooks/, which isn’t version-controlled, and they vary by language and OS.
Git hooks managers solve this problem. They let you define hooks in a configuration file that lives in your repository, install them automatically for every developer, and support multi-language toolchains. This guide compares the three leading options: pre-commit, Lefthook, and Husky.
Why Use a Git Hooks Manager
Without a hooks manager, every developer on your team must manually install hooks. This leads to inconsistent enforcement — some developers run linters before committing, others don’t. Broken code slips through. Hooks are also language-specific: a Python project might want black and flake8, while a TypeScript project needs eslint and prettier.
A hooks manager provides:
- Version-controlled configuration — your hooks live in a
.pre-commit-config.yaml,lefthook.yml, orhusky/directory that ships with the repo - Automatic installation — one command sets up all hooks for new developers
- Multi-language support — run Python linters, JavaScript formatters, and shell checkers in a single pipeline
- Consistent enforcement — every developer runs the same checks before every commit
- CI/CD integration — the same hooks that run locally can run in your CI pipeline
For teams that care about code quality, this is essential infrastructure. If you’re already running megalinter or super-linter in CI, running the same checks locally via git hooks catches issues before they ever reach the pipeline.
Pre-commit: The Python Powerhouse
pre-commit is the most widely adopted git hooks framework, with over 15,200 stars on GitHub. Written in Python, it uses a YAML configuration file and supports hooks written in any language through isolated virtual environments.
Installation
| |
Configuration
The configuration lives in .pre-commit-config.yaml at the repository root:
| |
Install the hooks with:
| |
Running Hooks
| |
Key Strengths
- Massive ecosystem — hundreds of ready-to-use hooks in the pre-commit-hooks repository and third-party repos
- Language isolation — each hook runs in its own virtual environment, avoiding dependency conflicts
- CI integration —
pre-commit run --all-filesworks identically in CI and locally - Auto-fix support — hooks like
blackandisortcan automatically fix issues and re-stage files
Limitations
- Python dependency — requires Python on every developer’s machine
- Slower execution — virtual environment setup adds overhead; large repos can see 10-30 second hook runs
- Complex config — the YAML syntax has many options and can become verbose
Lefthook: The Speed Demon
Lefthook is a fast, Go-based git hooks manager designed for performance. With 8,000+ stars, it’s the lightweight alternative that runs hooks in parallel and supports any project type.
Installation
| |
Configuration
The configuration lives in lefthook.yml:
| |
Install with:
| |
Advanced Features
Lefthook supports templates, pipelines, and conditional execution:
| |
Key Strengths
- Extreme speed — written in Go, runs hooks in parallel, no virtual environment overhead
- Single binary — no language runtime required; just download and run
- Flexible glob matching — target specific file types per command
- Conditional execution — skip hooks during merge/rebase, or only run when certain files change
- Templates and pipelines — define complex hook workflows with dependencies
Limitations
- Smaller ecosystem — fewer pre-built hooks compared to pre-commit
- Manual tool management — you’re responsible for installing linters and formatters
- Less CI integration — no built-in CI workflow like pre-commit’s
run --all-files
Husky: The JavaScript Standard
Husky is the most popular git hooks manager in the JavaScript ecosystem with nearly 35,000 stars. Modern Husky (v9+) is dramatically simplified from earlier versions — it no longer has its own config file and instead integrates directly with your existing package.json scripts.
Installation
| |
Configuration
Husky creates a .husky/ directory with hook scripts. Each file is a shell script:
| |
| |
You can also define hooks in package.json:
| |
Combine with lint-staged for staged-file filtering:
| |
| |
Key Strengths
- Zero config philosophy — just npm scripts, no separate YAML or TOML
- JavaScript ecosystem — seamless integration with npm/yarn/pnpm workflows
- lint-staged synergy — the de facto combination for JS/TS projects
- Massive adoption — used in nearly every modern JS project
Limitations
- Node.js required — won’t work without a JavaScript runtime
- No multi-language support — designed specifically for JS/TS ecosystems
- Less structured config — shell scripts in
.husky/are harder to review than YAML - Version migration pain — v8 to v9 was a breaking change that required config rewrites
Comparison Table
| Feature | Pre-commit | Lefthook | Husky |
|---|---|---|---|
| Language | Python | Go | JavaScript |
| Stars | 15,200+ | 8,000+ | 35,000+ |
| Config format | YAML | YAML | Shell scripts + package.json |
| Multi-language | Yes (isolated envs) | Yes (any binary) | No (JS-focused) |
| Parallel execution | Limited (by design) | Yes (built-in) | Via lint-staged |
| Auto-fix and re-stage | Yes | Manual | Via lint-staged |
| Ecosystem size | Large (100+ repos) | Small (DIY) | Medium (npm ecosystem) |
| CI integration | Excellent | Manual | Manual |
| Performance | Moderate | Excellent | Good |
| Best for | Python, multi-language, teams | Performance-critical, polyglot | JavaScript/TypeScript projects |
Performance Benchmarks
For a medium-sized project with 500+ files, here’s how the tools compare when running linting, formatting, and secret scanning on all staged files:
| Tool | Cold start | Warm run | Parallel |
|---|---|---|---|
| Pre-commit | 8-12s | 4-6s | Sequential |
| Lefthook | 1-2s | <1s | Parallel |
| Husky + lint-staged | 3-5s | 2-3s | Sequential |
Lefthook’s Go implementation gives it a clear speed advantage, especially on first run. Pre-commit’s virtual environment creation adds overhead, though cached environments reduce this on subsequent runs. Husky sits in the middle — the npm ecosystem tools are fast, but lint-staged doesn’t parallelize by default.
Which Tool Should You Choose?
Choose Pre-commit if:
- You work in a Python or multi-language environment
- You want a large ecosystem of pre-built hooks
- You need CI/CD parity — same hooks locally and in CI
- You want language isolation to avoid dependency conflicts
- Your team needs auto-fix and re-stage functionality
Choose Lefthook if:
- Speed is your top priority
- You want a single binary with no runtime dependencies
- You need fine-grained control over which hooks run when
- You work in a polyglot codebase and want to orchestrate multiple toolchains
- You want conditional execution (skip during merge, only on file changes)
Choose Husky if:
- You’re in a JavaScript/TypeScript project
- You want zero additional config beyond npm scripts
- Your team already uses lint-staged
- You want the most widely adopted solution in the JS ecosystem
- You’re building a React, Next.js, or Node.js application
Hybrid Approach
Many teams use multiple tools. For example:
- Lefthook as the orchestrator with pre-commit hooks called as commands
- Husky for JS linting plus a pre-push hook that calls external tools
- Pre-commit for Python plus Lefthook for Go and shell scripts
Integrating With Your CI/CD Pipeline
Git hooks catch issues locally, but CI enforcement ensures nothing slips through. The recommended approach:
| |
This ensures the same checks run in CI as locally. For teams using Woodpecker CI or Drone CI, the equivalent pipeline looks nearly identical.
For security-conscious teams, adding secret scanning with gitleaks or trufflehog as a pre-commit hook prevents credentials from ever entering your repository history.
FAQ
What is the difference between git hooks and a hooks manager?
Git hooks are shell scripts stored in .git/hooks/ that run at specific points (pre-commit, post-merge, etc.). They are not version-controlled and must be installed manually on each machine. A hooks manager (pre-commit, Lefthook, Husky) lets you define hooks in a version-controlled config file and install them automatically with a single command.
Can I use pre-commit with a JavaScript project?
Yes. Pre-commit supports any language through its hook system. You can configure it to run eslint, prettier, and other JS tools. However, Husky + lint-staged is more commonly used in JavaScript projects because of tighter npm ecosystem integration.
Does Lefthook support monorepos?
Yes. Lefthook’s glob matching and file filtering work well in monorepo setups. You can define different hooks for different directories using the glob and files options, and its parallel execution keeps hook times reasonable even in large monorepos.
Can I skip hooks for a specific commit?
Yes. With pre-commit, use SKIP=hook_id git commit -m "message". With Lefthook, use LEFTHOOK=0 git commit -m "message". With Husky, you can set HUSKY=0 as an environment variable. However, skipping hooks should be rare — they exist to maintain code quality.
Do git hooks slow down my development workflow?
Well-configured hooks should add only 1-3 seconds to your commit process. Focus on fast checks (linting, formatting, type checks) for pre-commit hooks. Move slower checks (full test suites, integration tests) to pre-push hooks or CI pipelines. Lefthook is the fastest option for time-sensitive workflows.
How do I migrate from one hooks manager to another?
First, uninstall the current hooks manager (pre-commit uninstall, lefthook uninstall, or remove .husky/ directory). Then install the new manager and recreate your hook configuration. Test with a small commit before rolling out to the team. Both pre-commit and Lefthook support the same git hook types (pre-commit, commit-msg, pre-push), so migration is mostly a config translation exercise.