Git hooks enforcement — running linters, formatters, and quality checks automatically before commits and pushes — is a foundational developer experience practice that prevents quality issues from entering the codebase. Husky has been the default Node.js tool for years, but Lefthook has emerged as a faster, more capable alternative with better monorepo and cross-language support. This guide compares both tools directly to help teams decide which to adopt or whether to migrate.
Why Git Hooks Matter for Developer Experience
Git hooks run scripts automatically at defined points in the git workflow — most commonly pre-commit (before a commit is created) and pre-push (before changes are pushed to the remote). For developer experience, the primary use cases are: running linters (ESLint, Biome, RuboCop) on staged files, applying code formatters (Prettier, gofmt) automatically, running type checks (tsc --noEmit), and executing fast unit tests to catch obvious regressions before they leave the developer's machine.
The alternative — relying purely on CI to catch quality issues — creates a slower feedback loop: the developer finishes work, pushes, waits for CI, gets notified of a linting error, fixes it, pushes again. Git hooks collapse this to immediate feedback at commit time, catching issues in seconds rather than minutes. The investment in hook configuration pays back with every prevented CI failure.
Husky: The Established Standard
Husky has been the dominant git hooks manager for Node.js projects since 2016. Version 8 simplified the configuration significantly, dropping the JSON config in favour of shell script files in a .husky/ directory. The setup is straightforward: npm install husky, add a prepare script, and create hook files.
Husky's strengths are its simplicity, extensive documentation, and the fact that virtually every JavaScript/TypeScript developer knows it. If you're looking at a new project and see a .husky/ directory, you immediately know what it is and how to read it. Integration with lint-staged (running lint tools only on staged files rather than the full codebase) is the standard Husky companion pattern.
Husky limitations: It requires Node.js, making it awkward for polyglot projects where not all contributors use Node. It has no built-in parallelisation — hooks run commands sequentially by default. It has no built-in support for selective hook execution based on file changes (lint-staged provides this but as a separate dependency). And performance on large monorepos with many hooks can be slow.
Lefthook: The Modern Challenger
Lefthook is a git hooks manager written in Go, distributed as a single binary with no runtime dependency on Node.js, Ruby, or any other language runtime. Configuration lives in a single YAML file (lefthook.yml) that supports parallel command execution, glob-based file filtering, and conditional execution natively.
The Go binary approach makes Lefthook fast — hook execution is 10–20× faster than equivalent Husky configurations on large projects, primarily due to Go's startup overhead being negligible compared to Node.js startup time. In projects where pre-commit hooks were taking 3–5 seconds (causing developers to avoid committing frequently), Lefthook typically reduces this to under 500ms.
Lefthook's YAML configuration is more expressive than Husky's shell scripts: parallel execution of independent checks (run ESLint and TypeScript check simultaneously), glob patterns for file-type-specific hooks, and tags for grouping and selectively running hooks are all built in.
| Dimension | Husky v9 | Lefthook v1 |
|---|---|---|
| Runtime dependency | Node.js required | None (single binary) |
| Config format | Shell scripts in .husky/ | Single lefthook.yml |
| Parallel execution | No (requires manual background processes) | Yes (built-in parallel: true) |
| Staged files filter | Via lint-staged (separate package) | Built-in glob: patterns |
| Performance | ~2–8s pre-commit (Node startup) | ~0.2–1s pre-commit (Go binary) |
| Monorepo support | Manual scripting | Native workspace support |
| Package managers | npm, yarn, pnpm | npm, yarn, pnpm, gem, go get, brew, binary |
| Ecosystem familiarity | Very high (industry default) | Growing (strong in polyglot teams) |
Configuration Examples
A typical Husky pre-commit setup with lint-staged:
.husky/pre-commit calls npx lint-staged; package.json defines lint-staged config mapping file globs to commands. Pre-commit runs sequentially: lint-staged processes staged files, then the hook completes.
The equivalent Lefthook configuration in a single lefthook.yml:
Define pre-commit with multiple commands under it, each with a glob for file filtering and run for the command. Set parallel: true at the hook level to run ESLint and TypeScript checks simultaneously — cutting wall-clock time roughly in half for independent checks.
When to Migrate and How
Migration from Husky to Lefthook is worthwhile when: hook execution time is causing developers to use --no-verify to bypass slow hooks (defeating the purpose); the project is polyglot and Node.js is not universally available; or monorepo complexity has made the Husky + lint-staged combination difficult to maintain. The migration is straightforward: install Lefthook, translate the .husky/ shell scripts to lefthook.yml commands, remove Husky and lint-staged, and test. Most migrations complete in 1–2 hours for a typical project.