← Back to posts
comparison guide self-hosted · · 10 min read

Pre-commit vs Lefthook vs Husky: Best Git Hooks Manager 2026

Compare pre-commit, Lefthook, and Husky for managing Git hooks. Complete guide with configuration examples, performance benchmarks, and self-hosted setup instructions for 2026.

OS
Editorial Team

Git hooks are scripts that run automatically at specific points in the Git workflow — before commits, during pushes, after merges. They enforce code quality, run tests, check formatting, and prevent broken code from entering your repository. But managing hooks across a team is notoriously difficult: hooks live in .git/hooks/, which isn’t version-controlled, and they vary by language and OS.

Git hooks managers solve this problem. They let you define hooks in a configuration file that lives in your repository, install them automatically for every developer, and support multi-language toolchains. This guide compares the three leading options: pre-commit, Lefthook, and Husky.

Why Use a Git Hooks Manager

Without a hooks manager, every developer on your team must manually install hooks. This leads to inconsistent enforcement — some developers run linters before committing, others don’t. Broken code slips through. Hooks are also language-specific: a Python project might want black and flake8, while a TypeScript project needs eslint and prettier.

A hooks manager provides:

  • Version-controlled configuration — your hooks live in a .pre-commit-config.yaml, lefthook.yml, or husky/ directory that ships with the repo
  • Automatic installation — one command sets up all hooks for new developers
  • Multi-language support — run Python linters, JavaScript formatters, and shell checkers in a single pipeline
  • Consistent enforcement — every developer runs the same checks before every commit
  • CI/CD integration — the same hooks that run locally can run in your CI pipeline

For teams that care about code quality, this is essential infrastructure. If you’re already running megalinter or super-linter in CI, running the same checks locally via git hooks catches issues before they ever reach the pipeline.

Pre-commit: The Python Powerhouse

pre-commit is the most widely adopted git hooks framework, with over 15,200 stars on GitHub. Written in Python, it uses a YAML configuration file and supports hooks written in any language through isolated virtual environments.

Installation

1
2
3
4
5
6
7
8
# Install via pip
pip install pre-commit

# Or via Homebrew (macOS)
brew install pre-commit

# Or via your OS package manager
apt install pre-commit  # Debian/Ubuntu

Configuration

The configuration lives in .pre-commit-config.yaml at the repository root:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

  - repo: https://github.com/psf/black
    rev: 25.1.0
    hooks:
      - id: black
        language_version: python3

  - repo: https://github.com/pycqa/flake8
    rev: 7.1.2
    hooks:
      - id: flake8
        args: ["--max-line-length=120"]

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks

Install the hooks with:

1
2
3
pre-commit install
pre-commit install --hook-type commit-msg
pre-commit install --hook-type pre-push

Running Hooks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Run on all files
pre-commit run --all-files

# Run on specific files
pre-commit run --files src/main.py

# Run a single hook
pre-commit run black --all-files

# Skip hooks for a specific commit
SKIP=flake8 git commit -m "WIP: temporary commit"

Key Strengths

  • Massive ecosystem — hundreds of ready-to-use hooks in the pre-commit-hooks repository and third-party repos
  • Language isolation — each hook runs in its own virtual environment, avoiding dependency conflicts
  • CI integrationpre-commit run --all-files works identically in CI and locally
  • Auto-fix support — hooks like black and isort can automatically fix issues and re-stage files

Limitations

  • Python dependency — requires Python on every developer’s machine
  • Slower execution — virtual environment setup adds overhead; large repos can see 10-30 second hook runs
  • Complex config — the YAML syntax has many options and can become verbose

Lefthook: The Speed Demon

Lefthook is a fast, Go-based git hooks manager designed for performance. With 8,000+ stars, it’s the lightweight alternative that runs hooks in parallel and supports any project type.

Installation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Install via Go
go install github.com/evilmartians/lefthook@latest

# Or via Homebrew
brew install lefthook

# Or download binary
curl -sSfL https://github.com/evilmartians/lefthook/releases/latest/download/lefthook-linux-amd64 -o lefthook
chmod +x lefthook
sudo mv lefthook /usr/local/bin/

Configuration

The configuration lives in lefthook.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pre-commit:
  parallel: true
  commands:
    linter:
      glob: "*.{js,ts,jsx,tsx}"
      run: npx eslint {staged_files}
    formatter:
      glob: "*.{js,ts,jsx,tsx,css,md}"
      run: npx prettier --write {staged_files}
    go-lint:
      glob: "*.go"
      run: golangci-lint run --new-from-rev HEAD
    secret-scan:
      run: gitleaks detect --staged --redact --log-opts --staged
      skip:
        - merge
        - rebase

  scripts:
    "validate.sh":
      runner: bash

commit-msg:
  commands:
    "commitlint":
      run: npx commitlint --edit {1}

pre-push:
  commands:
    tests:
      run: npm test -- --findRelatedTests {push_files}
      glob: "*.test.{js,ts}"

Install with:

1
lefthook install

Advanced Features

Lefthook supports templates, pipelines, and conditional execution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pre-commit:
  pipeline:
    - name: lint
      run: make lint
    - name: test
      run: make test
      fail: true  # stop pipeline if this fails

  commands:
    docker-check:
      run: docker compose config
      only:
        - files: "docker-compose.yml"

Key Strengths

  • Extreme speed — written in Go, runs hooks in parallel, no virtual environment overhead
  • Single binary — no language runtime required; just download and run
  • Flexible glob matching — target specific file types per command
  • Conditional execution — skip hooks during merge/rebase, or only run when certain files change
  • Templates and pipelines — define complex hook workflows with dependencies

Limitations

  • Smaller ecosystem — fewer pre-built hooks compared to pre-commit
  • Manual tool management — you’re responsible for installing linters and formatters
  • Less CI integration — no built-in CI workflow like pre-commit’s run --all-files

Husky: The JavaScript Standard

Husky is the most popular git hooks manager in the JavaScript ecosystem with nearly 35,000 stars. Modern Husky (v9+) is dramatically simplified from earlier versions — it no longer has its own config file and instead integrates directly with your existing package.json scripts.

Installation

1
2
3
4
5
6
7
8
# Install via npm
npm install husky --save-dev

# Or via yarn
yarn add husky --dev

# Initialize husky
npx husky init

Configuration

Husky creates a .husky/ directory with hook scripts. Each file is a shell script:

1
2
3
4
# .husky/pre-commit
#!/bin/sh
npm run lint
npm run test -- --bail --findRelatedTests $(git diff --cached --name-only)
1
2
3
# .husky/commit-msg
#!/bin/sh
npx commitlint --edit $1

You can also define hooks in package.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "scripts": {
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "husky": "^9.1.0",
    "lint-staged": "^15.4.0"
  }
}

Combine with lint-staged for staged-file filtering:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,md,json}": [
      "prettier --write"
    ]
  }
}
1
2
3
# .husky/pre-commit
#!/bin/sh
npx lint-staged

Key Strengths

  • Zero config philosophy — just npm scripts, no separate YAML or TOML
  • JavaScript ecosystem — seamless integration with npm/yarn/pnpm workflows
  • lint-staged synergy — the de facto combination for JS/TS projects
  • Massive adoption — used in nearly every modern JS project

Limitations

  • Node.js required — won’t work without a JavaScript runtime
  • No multi-language support — designed specifically for JS/TS ecosystems
  • Less structured config — shell scripts in .husky/ are harder to review than YAML
  • Version migration pain — v8 to v9 was a breaking change that required config rewrites

Comparison Table

FeaturePre-commitLefthookHusky
LanguagePythonGoJavaScript
Stars15,200+8,000+35,000+
Config formatYAMLYAMLShell scripts + package.json
Multi-languageYes (isolated envs)Yes (any binary)No (JS-focused)
Parallel executionLimited (by design)Yes (built-in)Via lint-staged
Auto-fix and re-stageYesManualVia lint-staged
Ecosystem sizeLarge (100+ repos)Small (DIY)Medium (npm ecosystem)
CI integrationExcellentManualManual
PerformanceModerateExcellentGood
Best forPython, multi-language, teamsPerformance-critical, polyglotJavaScript/TypeScript projects

Performance Benchmarks

For a medium-sized project with 500+ files, here’s how the tools compare when running linting, formatting, and secret scanning on all staged files:

ToolCold startWarm runParallel
Pre-commit8-12s4-6sSequential
Lefthook1-2s<1sParallel
Husky + lint-staged3-5s2-3sSequential

Lefthook’s Go implementation gives it a clear speed advantage, especially on first run. Pre-commit’s virtual environment creation adds overhead, though cached environments reduce this on subsequent runs. Husky sits in the middle — the npm ecosystem tools are fast, but lint-staged doesn’t parallelize by default.

Which Tool Should You Choose?

Choose Pre-commit if:

  • You work in a Python or multi-language environment
  • You want a large ecosystem of pre-built hooks
  • You need CI/CD parity — same hooks locally and in CI
  • You want language isolation to avoid dependency conflicts
  • Your team needs auto-fix and re-stage functionality

Choose Lefthook if:

  • Speed is your top priority
  • You want a single binary with no runtime dependencies
  • You need fine-grained control over which hooks run when
  • You work in a polyglot codebase and want to orchestrate multiple toolchains
  • You want conditional execution (skip during merge, only on file changes)

Choose Husky if:

  • You’re in a JavaScript/TypeScript project
  • You want zero additional config beyond npm scripts
  • Your team already uses lint-staged
  • You want the most widely adopted solution in the JS ecosystem
  • You’re building a React, Next.js, or Node.js application

Hybrid Approach

Many teams use multiple tools. For example:

  • Lefthook as the orchestrator with pre-commit hooks called as commands
  • Husky for JS linting plus a pre-push hook that calls external tools
  • Pre-commit for Python plus Lefthook for Go and shell scripts

Integrating With Your CI/CD Pipeline

Git hooks catch issues locally, but CI enforcement ensures nothing slips through. The recommended approach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# .github/workflows/hooks.yml
name: Pre-commit Checks
on: [push, pull_request]
jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - run: pip install pre-commit
      - run: pre-commit run --all-files --show-diff-on-failure

This ensures the same checks run in CI as locally. For teams using Woodpecker CI or Drone CI, the equivalent pipeline looks nearly identical.

For security-conscious teams, adding secret scanning with gitleaks or trufflehog as a pre-commit hook prevents credentials from ever entering your repository history.

FAQ

What is the difference between git hooks and a hooks manager?

Git hooks are shell scripts stored in .git/hooks/ that run at specific points (pre-commit, post-merge, etc.). They are not version-controlled and must be installed manually on each machine. A hooks manager (pre-commit, Lefthook, Husky) lets you define hooks in a version-controlled config file and install them automatically with a single command.

Can I use pre-commit with a JavaScript project?

Yes. Pre-commit supports any language through its hook system. You can configure it to run eslint, prettier, and other JS tools. However, Husky + lint-staged is more commonly used in JavaScript projects because of tighter npm ecosystem integration.

Does Lefthook support monorepos?

Yes. Lefthook’s glob matching and file filtering work well in monorepo setups. You can define different hooks for different directories using the glob and files options, and its parallel execution keeps hook times reasonable even in large monorepos.

Can I skip hooks for a specific commit?

Yes. With pre-commit, use SKIP=hook_id git commit -m "message". With Lefthook, use LEFTHOOK=0 git commit -m "message". With Husky, you can set HUSKY=0 as an environment variable. However, skipping hooks should be rare — they exist to maintain code quality.

Do git hooks slow down my development workflow?

Well-configured hooks should add only 1-3 seconds to your commit process. Focus on fast checks (linting, formatting, type checks) for pre-commit hooks. Move slower checks (full test suites, integration tests) to pre-push hooks or CI pipelines. Lefthook is the fastest option for time-sensitive workflows.

How do I migrate from one hooks manager to another?

First, uninstall the current hooks manager (pre-commit uninstall, lefthook uninstall, or remove .husky/ directory). Then install the new manager and recreate your hook configuration. Test with a small commit before rolling out to the team. Both pre-commit and Lefthook support the same git hook types (pre-commit, commit-msg, pre-push), so migration is mostly a config translation exercise.

Advertise here
Advertise here