Goodbye Husky: Why I Switched to Lefthook for Git Hooks Management

Git tutorial - IT technology blog
Git tutorial - IT technology blog

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 add on 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.

Share: