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.yaml packages. 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-commit shell 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.