Background: The 2 AM CI/CD Failure
It was 2 AM, and Slack was blowing up because the production build pipeline failed. The cause was quite unusual: a developer on the team pushed Go code from a Windows machine but forgot to run linting. This error should have been blocked right on the local machine using Git Hooks.
Previously, I was loyal to Husky. However, trouble arose when the project grew into a Monorepo containing Python, Go, and Rust. Forcing backend developers to install Node.js just to run a Git Hook was an unreasonable request. Not to mention, Husky runs sequentially, making it extremely slow. With large commits, I sometimes had to wait over 30 seconds just for the linter to finish—a break long enough for me to lose focus and accidentally hit that disastrous git push --force command.
That is why I turned to Lefthook. Written in Go, Lefthook is a single binary, ultra-lightweight, and does not depend on the Node.js environment.
Installing Lefthook: Flexible for Every Workflow
Lefthook is not picky about the environment. You can integrate it into your project in several ways.
Method 1: For Node.js Projects
npm install lefthook --save-dev
# Or
yarn add -D lefthook
Method 2: Standalone Installation (For Go, Python, Rust…)
If the project doesn’t use package.json, you can install it directly onto your system:
# macOS (Homebrew)
brew install lefthook
# Install via Go
go install github.com/evilmartians/lefthook@latest
After installation, initialize it with the command: lefthook install. A lefthook.yml file will appear in the root directory. This is where you control your entire commit workflow.
Configuration: The Power of Parallel Execution
The biggest selling point of Lefthook is its extremely clear YAML configuration file. Here is how I set it up to check the JS linter and format Go code simultaneously:
# lefthook.yml
pre-commit:
parallel: true # Run commands concurrently to save time
commands:
linter-js:
glob: "*.{js,ts,tsx}" # Filter only changed files
run: npx eslint {staged_files} --fix
formatter-go:
root: "backend/"
glob: "*.go"
run: go fmt {staged_files} && git add {staged_files}
check-secret:
run: gitleaks detect --source . --verbose
Why is this configuration better than Husky?
- parallel: true: Husky runs commands one by one. Lefthook maximizes CPU usage to run them in parallel. If you have 5 tasks, the waiting time drops from 30 seconds to about 5-7 seconds.
- {staged_files}: Instead of scanning the entire project, Lefthook only passes the list of files you just ran
git addon into the command. This makes processing many times faster. - Glob filters: You can specify exactly which linter runs for which file type, avoiding wasted resources.
Real-world Scenarios
Sometimes you need to commit urgently to handle an incident (hotfix) without waiting for the linter to check minor style errors.
Skipping Hooks Professionally
Instead of using the unsafe --no-verify flag, Lefthook allows you to skip specific commands via environment variables:
LEFTHOOK=0 git commit -m "Emergency hotfix"
Or just skip the secret check if you are sure:
SKIP=check-secret git commit -m "Temporary commit"
Ensuring Team-wide Compliance
To prevent colleagues from “forgetting” to install hooks, add this script to package.json:
{
"scripts": {
"postinstall": "lefthook install"
}
}
Every time npm install is run, Lefthook will automatically re-establish the hooks in .git/hooks. No one can “dodge” the code checks before pushing to the server.
Field Experience: Don’t Lose Money to Leaked Secrets
You can test the configuration at any time using the command: lefthook run pre-commit. This command simulates the commit process and prints detailed logs. If there is an error, Lefthook will clearly indicate which command failed instead of throwing out a mess of chaotic logs.
A hard-learned lesson of mine: Always include gitleaks in the pre-commit step. I once accidentally pushed an AWS Secret Key to GitHub and received a warning from Amazon just 5 minutes later. Since then, the check-secret step has been mandatory in all my projects.
Conclusion
Husky is still good for small, pure JavaScript projects. But if you want speed, flexibility for Monorepos, and stability for large teams, Lefthook is an irreplaceable choice. The transition takes only 15 minutes but will save you hours of waiting and prevent unnecessary build failures.

