DEV Community

Cover image for Saying Goodbye to Husky: How Lefthook Supercharged Our TypeScript Workflow
Yoshihide Shiono
Yoshihide Shiono

Posted on

Saying Goodbye to Husky: How Lefthook Supercharged Our TypeScript Workflow

Introduction: The Unseen Impact of Git Hooks on DX

In modern Web development, maintaining code quality isn't just about "being careful" — it's about building systems that make quality inevitable. Git hooks are the unsung heroes of this mission.

For years, the gold standard for managing Git hooks in the Node.js ecosystem has been the duo of Husky and lint-staged. But as projects grow, architectures shift toward microservices, and monorepos become the norm, we’ve started to see some cracks in this "classic" setup.

We’ve all been there: you hit git commit, and then... you wait. You wait for several seconds—sometimes ten or twenty — while the checks run. We might tell ourselves it’s just a short break, but these tiny interruptions add up. They break our "flow state" and create friction in our daily rhythm.

In this post, I want to walk you through why we decided to solve this friction by migrating to Lefthook — a lightning-fast Git hook manager written in Go.

The Limits of the Past: Why Husky + lint-staged Can Feel "Heavy"

While Husky and lint-staged have served us well, they carry three specific burdens that become more apparent as a project scales:

① Node.js Runtime Overhead

Both Husky and lint-staged are built on Node.js. Every time you commit, the system has to spin up the Node runtime, load packages, and interpret scripts. For a single commit, it’s a blink of an eye. For an engineer committing dozens of times a day, it’s a cumulative tax on productivity.

② Sequential Execution Bottlenecks

By default, lint-staged usually runs commands one after another. It finishes ESLint, then starts Prettier, and so on. Even though our modern laptops have plenty of CPU cores, this sequential approach leaves that power untapped.

③ Scattered Configuration

Since Husky v8, configurations moved into shell scripts inside the .husky/ directory. Meanwhile, your file filtering logic (the lint-staged part) lives in package.json or .lintstagedrc. Having your logic split across multiple files makes it harder to see the "big picture" of what’s happening during a commit, increasing the mental load for maintenance.

Enter Lefthook: The Next-Gen Choice

Lefthook, created by the team at Evil Martians, is a Git hook manager written in Go. Its philosophy is simple: speed and simplicity.

Four Ways Lefthook Changes the Game:

  1. Native Binary Speed: Because it’s a Go binary, there’s no waiting for Node.js to start. It’s nearly instantaneous.
  2. Parallel Execution by Default: Just add parallel: true to your config, and your commands start running on multiple threads simultaneously.
  3. Consolidated YAML Management: Everything — pre-commit, pre-push, commit-msg — lives in a single lefthook.yml file.
  4. Language Agnostic: Whether you’re in a monorepo or a polyglot environment (Node, Go, Python, Rust), Lefthook handles it all with one consistent configuration.

The Migration Guide: From Husky to Lefthook

Ready to make the switch? Let's walk through the process using a modern TypeScript/Node.js project as our example.

STEP 1: The Cleanup

First, we say a respectful goodbye to our good old tools.

# Remove the old dependencies
npm uninstall husky lint-staged

# Delete the old configuration files

rm -rf .husky
rm .lintstagedrc # (if it exists)
Enter fullscreen mode Exit fullscreen mode

Don’t forget to remove any prepare: "husky install" scripts from your package.json!

STEP 2: Install and Initialize Lefthook

Now, let’s bring in the new speed. For Node.js projects, installing via npm ensures everyone on your team stays in sync.

npm install lefthook --save-dev
npx lefthook install
Enter fullscreen mode Exit fullscreen mode

STEP 3: Designing your lefthook.yml

This is where the magic happens. We’re going to build a configuration that replaces both Husky and lint-staged in one go.

# lefthook.yml

# --- pre-commit: Quality checks before the commit is finalized ---
pre-commit:
  parallel: true # Run everything at once!
  commands:
    # (1) Static analysis and auto-fixing with ESLint
    eslint:
      # {staged_files} targets only what you've added (replacing lint-staged)
      run: npx eslint {staged_files} --fix && git add {staged_files}
      glob: '*.{ts,tsx,js,jsx}'

    # (2) Code formatting with Prettier
    prettier:
      run: npx prettier --write {staged_files} && git add {staged_files}
      glob: '*.{ts,tsx,js,jsx,json,css,md,yml}'

    # (3) TypeScript Type Checking (Check the whole project)
    # Type checking is heavy, so parallel execution is a huge win here.
    type-check:
      run: npx tsc --noEmit

# --- commit-msg: Enforcing commit message standards ---
commit-msg:
  commands:
    'lint-commit-message':
      # {1} is the path to the temporary file containing your commit message
      run: npx commitlint --edit {1}

# --- pre-push: The final line of defense ---
pre-push:
  commands:
    unit-test:
      run: npm test
Enter fullscreen mode Exit fullscreen mode

Why Parallel Matters

In this setup, eslint, prettier, and type-check all start at the same time. While tsc --noEmit is notoriously slow, it no longer makes you wait for the linter to finish first. Your total wait time is now just the duration of your slowest task, not the sum of all of them.

Advanced Techniques for the Real World

① Mastering Monorepos

If you have a frontend and backend in one repository, the root option is your best friend.

pre-commit:
  commands:
    frontend-lint:
      run: npm run lint {staged_files}
      root: 'packages/frontend/'
      glob: '*.{ts,tsx}'
Enter fullscreen mode Exit fullscreen mode

② Emergency Skips

We’ve all had those "fix it now" moments. Lefthook makes it easy to skip hooks with environment variables.

# Skip everything
LEFTHOOK=0 git commit -m "Emergency fix"

# Skip just the linter
SKIP=eslint git commit -m "Skip lint for this one"
Enter fullscreen mode Exit fullscreen mode

③ Personal Overrides with lefthook-local.yml

Sometimes you want a custom hook just for yourself (like a Slack notification or a specific local script). You can create a lefthook-local.yml, add it to your .gitignore, and Lefthook will merge it with the team settings. It’s the perfect way to customize your workflow without bothering your teammates.

Closing Thoughts: Better DX, One Commit at a Time

Switching from Husky to Lefthook is about more than just swapping libraries. It’s an investment in your team’s Development Experience (DX). It’s about removing those tiny "micro-frictions" that add up over a workday.

The Benefits:

  • Time Saved: Parallel execution can cut commit waits by 50% or more.
  • Clarity: One YAML file that anyone can read and understand.
  • Future-Proof: Built-in support for monorepos and CI/CD.

Migrating takes about 10 minutes, but the speed you gain will stay with you for every commit to come. Give it a try — your flow state will thank you!

Top comments (0)