Stage 2 · Tools
Hooks & Automation
Lefthook & Husky
Version-control your Git hooks and run them fast — Lefthook for speed, Husky for simplicity.
Why Manage Hooks?
Raw Git hooks in .git/hooks/ have two problems: they are not tracked by Git, and every developer on your team must set them up manually. If a new developer forgets to copy a hook, that person's commits bypass all team standards. A hook manager solves both problems — it stores hooks in version control and installs them automatically when developers clone or install dependencies.
- Hooks are stored in version control (not in
.git/hooks/). - New team members get hooks automatically on install.
- Hooks can run in parallel for speed.
- Configuration is centralized in a single file.
Lefthook
Lefthook is a hook manager written in Go. It is fast, cross-platform, and supports parallel execution out of the box. It uses a single lefthook.toml file for configuration and can run multiple hook scripts simultaneously.
# Install Lefthook (Go binary)
brew install Lefthook/homebrew-lefthook/lefthook
# Or via npm (for Node.js projects)
npm install --save-dev lefthook
# Initialize in your repo
lefthook installThe lefthook install command creates the Git hook scripts in .git/hooks/ that delegate to the Lefthook runner. This is a one-time setup per clone.
Lefthook Configuration
All Lefthook configuration lives in lefthook.toml at the repo root. Each key in the file maps to a Git hook name. Inside each hook, you define commands — individual scripts that run for that hook.
[pre-commit]
parallel = true
jobs:
lint:
glob: "*.{ts,tsx,js,jsx}"
run: npx eslint --fix {staged_files}
stage_fixed: true
format:
glob: "*.{ts,tsx,js,jsx,json,css}"
run: npx prettier --write {staged_files}
stage_fixed: true
typecheck:
glob: "*.{ts,tsx}"
run: npx tsc --noEmit
[commit-msg]
jobs:
commitlint:
run: npx commitlint --edit {1}
[pre-push]
jobs:
test:
run: npx vitest run
tags: [test]This configuration defines three hooks. The pre-commit hook runs lint, format, and typecheck in parallel. The commit-msg hook validates the commit message format. The pre-push hook runs the test suite before allowing a push.
parallel = true— Run all commands simultaneously instead of sequentially.glob— Only run the command if staged files match the pattern.{staged_files}— A Lefthook variable that expands to the list of staged files.stage_fixed: true— Automatically re-stage files that Lefthook modifies (e.g., after formatting).
When a hook runs a formatter like Prettier, the file changes but stays unstaged. Setting stage_fixed: true re-stages the file so the commit includes the formatted version. This keeps your hook transparent to the developer.
Husky
Husky is a lightweight hook manager popular in the JavaScript ecosystem. It stores hook scripts as plain shell files in a .husky/ directory, making them easy to read and edit. It works with npm, yarn, and pnpm.
# Install Husky
npm install --save-dev husky
# Initialize Husky in your repo
npx husky init
# This creates a .husky/ directory and sets up
# the prepare script in package.json.npx husky init creates the .husky/ directory, adds a sample pre-commit hook, and adds the prepare script to package.json so hooks are installed automatically on npm install.
Husky Setup
Each hook is a separate file in .husky/. The file name matches the Git hook name (e.g., pre-commit, commit-msg). The file is a plain shell script.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-stagedThis hook runs lint-staged, which executes linters and formatters only on staged files. The . husky.sh line sources Husky's internal setup for proper exit code handling.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit "$1"This hook runs commitlint to validate the commit message against Conventional Commits. The $1 argument is the path to the temporary file containing the commit message.
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}lint-staged runs the specified commands on only the staged files matching each glob pattern. This is much faster than running linters on the entire project.
Husky v9 simplified the setup — hooks no longer need the husky.sh sourcing line in most cases, and npx husky init handles everything. Check your version for the latest conventions.
Lefthook vs Husky
| Feature | Lefthook | Husky |
|---|---|---|
| Language | Go (binary) | Shell scripts |
| Configuration | lefthook.toml (single file) | .husky/ (one file per hook) |
| Parallel execution | Built-in (parallel = true) | Not built-in (use lint-staged) |
| Glob filtering | Built-in (glob pattern per command) | Via lint-staged |
| Auto-re-stage | stage_fixed: true | Via lint-staged |
| Speed | Faster (Go binary, parallel) | Slightly slower (shell, sequential) |
| Ecosystem | Language-agnostic | JavaScript/Node.js focused |
| Install trigger | lefthook install (manual or postinstall) | prepare script in package.json |
Choose Lefthook if you want maximum speed, parallel execution, and a language-agnostic setup. Choose Husky if your team is all JavaScript and you want a simpler, more familiar configuration.
Both Lefthook and Husky hooks are local — they run on the developer's machine. For CI, you need a separate workflow (GitHub Actions, GitLab CI, etc.) that runs the same linting, testing, and validation steps. Don't rely on hooks alone for quality gates.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.