Install
# Single binary
curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.deb.sh' \
| sudo bash
sudo apt install lefthook
# macOS
brew install lefthook
# Or via npm / cargo / mise
npm install -g lefthook
cargo install lefthook
mise use -g lefthook@latest
Configure in the repo root
# lefthook.yml
pre-commit:
parallel: true
commands:
rust-fmt:
glob: "*.rs"
run: cargo fmt -- --check {staged_files}
rust-clippy:
glob: "*.rs"
run: cargo clippy -- -D warnings
eslint:
glob: "*.{js,ts,jsx,tsx}"
run: npx eslint {staged_files}
prettier:
glob: "*.{js,ts,jsx,tsx,css,json,md}"
run: npx prettier --check {staged_files}
python-ruff:
glob: "*.py"
run: ruff check {staged_files}
python-format:
glob: "*.py"
run: ruff format --check {staged_files}
yaml-validate:
glob: "*.{yml,yaml}"
run: yamllint {staged_files}
no-large-files:
run: |
for f in {staged_files}; do
size=$(wc -c <"$f")
if [ "$size" -gt 1000000 ]; then
echo "$f is >1MB; consider git-lfs or excluding"
exit 1
fi
done
commit-msg:
commands:
lint-commit:
run: npx --no-install commitlint --edit {1}
pre-push:
parallel: true
commands:
test:
run: cargo test
typecheck:
run: tsc --noEmit
Install hooks into .git/
lefthook install
This writes the Lefthook-managed shims into .git/hooks/ — one per Git hook (pre-commit, pre-push, etc.). Each shim invokes lefthook with the appropriate config section.
Commit the lefthook.yml and (optional) a small bootstrap doc in README.md so new contributors run lefthook install once.
What runs when you commit
git commit -m "WIP"
# Lefthook spawns all matching pre-commit commands in parallel
# │ rust-fmt [PASS] 0.3s
# │ rust-clippy [PASS] 4.2s
# │ eslint [PASS] 1.1s
# │ prettier [PASS] 0.4s
# │ python-ruff [PASS] 0.2s
# Total: 4.2s (longest single command, not sum)
Parallel execution + per-glob filtering = only the linters relevant to the staged files actually run, all at once. On a 5-language monorepo, this is dramatically faster than serial pre-commit hooks.
The {staged_files} placeholder
Each command is invoked with only the staged files that match its glob — eslint sees only the changed JS/TS files, not the entire src tree. Speeds up incremental linting on large repos by orders of magnitude.
Other placeholders worth knowing:
{all_files}— every file in the repo (for full-tree lint){push_files}— files in the push range (for pre-push hooks){1},{2}— positional args (for commit-msg, get the commit-message file path)
Per-language scoping
Lefthook's glob and file_types filters scope each command to relevant files. Combined with parallel execution, a single repo can have 20+ commands defined, but on any given commit only the 3-4 matching ones actually run.
commands:
go-test:
file_types: ["go"]
run: go test ./...
py-test:
files: "git ls-files | grep -E '\\.py$'"
run: pytest -x
shell-lint:
glob: "*.sh"
run: shellcheck {staged_files}
Skip + run-only
# Skip a hook for a specific commit (escape hatch)
LEFTHOOK=0 git commit -m "..."
# Or per-command
git commit -m "..." --no-verify
# Or per-language during dev (set in lefthook-local.yml, gitignored)
# lefthook-local.yml
pre-commit:
commands:
rust-clippy:
skip: true
lefthook-local.yml is gitignored by default; per-developer overrides without affecting the repo's shared config.
Integration with mise / pre-commit
For repos already on pre-commit's ecosystem (large public Python projects), Lefthook can call pre-commit:
pre-commit:
commands:
pre-commit:
run: pre-commit run --files {staged_files}
Use Lefthook as the runner, pre-commit's hook ecosystem for specific community-maintained checks.
CI: run the same hooks
# GitHub Actions
- uses: jdx/mise-action@v2
- run: lefthook run pre-commit --all-files
- run: lefthook run pre-push --all-files
Same config, same checks, same parallelism. CI catches what developers skipped via --no-verify.
Lefthook vs alternatives
- pre-commit (the Python tool) — the de-facto standard; huge ecosystem of
.pre-commit-hooks.yamlpackages. Slower to start; Python-dependent. Use if your team already has muscle memory. - husky — Node-only, fine for npm-only repos. For anything multi-language, less natural.
- Plain
.git/hooks/pre-commitshell script — works, doesn't share between developers (since.git/hooks/isn't versioned). Lefthook's install step solves the sharing problem. - overcommit (Ruby) — older; Ruby-dependent.
For polyglot teams that want fast hooks without language-runtime dependencies, Lefthook is the cleanest pick in 2026.