# Lefthook: A Single-Binary Git Hooks Manager for Polyglot Repositories

> Lefthook installs Git hooks from a lefthook.yml file and runs jobs in parallel, with glob filters, tags and per-developer overrides. It suits mixed frontend and backend repositories, but the README leaves error handling and rollback undocumented.

**evilmartians/lefthook** — Fast and powerful Git hooks manager for any type of projects.

- Repository: https://github.com/evilmartians/lefthook
- Website: http://lefthook.dev/
- Stars: 8,874 · Forks: 314
- Language: Go
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/evilmartians-lefthook

## The polyglot hook problem Lefthook targets

Git hooks are shell scripts in .git/hooks, and they are not committed. Every developer has to recreate them, and every language ecosystem ships its own installer: a Node project reaches for a package that writes a pre-commit script, a Ruby project reaches for another, and a repository containing both ends up with two installers fighting over the same directory. Lefthook's answer is one binary and one YAML file at the repository root. The README describes it as "a Git hooks manager for Node.js, Ruby, Python and many other types of projects", and it is distributed as a single dependency-free binary. That matters most in a monorepo where the frontend and backend live side by side: a single lefthook.yml can declare one job that runs yarn eslint over staged files and another that runs bundle exec rubocop, and both are installed by the same command. The tool is for teams that already run linters and want them enforced at commit time without asking each developer to configure their own hooks.

## How lefthook.yml becomes executable hooks

The mechanism is deliberately thin. The repository holds a lefthook.yml file; lefthook install reads it and writes hook scripts into .git/hooks that call back into the lefthook binary. From then on Git invokes lefthook, which parses the config and runs the jobs listed under the matching hook name. The README's example config shows the shape: a top-level key such as pre-commit or pre-push, then a jobs list, where each entry has a name, a run command and optional filters. Two filters do most of the work. A glob key limits a job to files matching a pattern, and an exclude list removes paths from that set. The run command can interpolate file lists: {staged_files} expands to what is staged, {all_files} to the whole tree, and {files} to the result of a custom command declared in the files key, such as git diff --name-only HEAD @{push}. There is also a root key that changes the working directory for a job, which the README warns should have a trailing slash. Parallelism is a per-hook flag: setting parallel: true under pre-push lets independent jobs run at the same time. Tags add a second axis of control, letting a developer exclude a group such as frontend in a local override file. None of this requires a runtime beyond the binary itself, which is why the same config works in a Go project and a Rails project.

## Installing Lefthook and running the first hook

Installation depends on the ecosystem you already use. The README lists Go (>= 1.26), npm, gem and pipx as first-class routes, with apt, brew and winget covered in the installation guide. For a Node project, the npm route is the shortest:

```bash
npm install lefthook --save-dev
```

That places the binary in node_modules/.bin, so it is available to npm scripts. Ruby and Python projects use gem install lefthook and pipx install lefthook respectively. Go users can install a pinned release directly:

```bash
go install github.com/evilmartians/lefthook/v2@v2.1.14
```

With the binary on PATH, create a lefthook.yml at the repository root. The README's own example runs a linter over staged files and a second one over the whole tree:

```yaml
pre-commit:
  jobs:
    - name: lint frontend
      run: yarn eslint {staged_files}
    - name: lint backend
      run: bundle exec rubocop --force-exclusion -- {all_files}
```

Then install the hooks into the Git project:

```bash
lefthook install
```

The README's quick start ends with git add -A && git commit -m '...' to trigger them. You do not have to commit to test a hook: lefthook run pre-commit executes the group directly, which is the fastest way to iterate on a config. The README also shows that arbitrary extra groups work the same way, so a fixer group with auto-correcting commands can be run on demand with lefthook run fixer.

## Files, tags and local overrides in real repositories

The filter system is where Lefthook earns its place in a large repository. A job with glob: "*.rb" only fires when a Ruby file is staged, and exclude can drop generated files such as routes.rb from the set. That keeps a pre-commit hook from running a full backend lint when only a stylesheet changed. Tags cut the other way: a job tagged frontend and linters can be skipped wholesale by a developer who only touches the backend, using a lefthook-local.yml file that the README documents as the place for overrides. The same file can set skip: true on a single job, which is the escape hatch for a check that is slow or broken on one machine. Local config is not committed, so the team default stays intact. The Docker example is worth noting because it changes the execution model rather than the filtering: a runner key of docker run -it --rm <container_id_or_name> {cmd} sends the job into a container instead of running it on the host. That is useful when the linter is only installed in an image, but it also means the hook depends on a running container, and the README does not discuss what happens when that container is absent.

## Where Lefthook is the wrong tool

The README is a usage document, not an operations manual, and the gaps are visible. There is no documented rollback: lefthook install writes into .git/hooks, and the README does not describe an uninstall command or how to recover if an existing hook script was overwritten. Teams that already have hand-written hooks should back them up before running install. Failure semantics are also thin. The README shows output configuration with execution and failure values, so you can control what is printed, but it does not spell out how a failing job affects the exit code of the hook or how partial failures across parallel jobs are reported. If your workflow depends on a precise contract there, you will have to verify it yourself. There is a Windows-specific caveat too: the Makefile's install target has a separate branch for Windows that copies lefthook.exe into GOPATH, and the README's primary install examples are Unix-oriented, so Windows users should treat the winget guide as the reference. Finally, if your team is homogeneous and already happy with an ecosystem-native hook runner, the extra config file and binary are overhead rather than a gain. Lefthook pays off when the alternative is maintaining several installers in one repository.

## Lefthook compared with Husky and pre-commit

The most common comparison is with Husky, the Node-based hook manager, and with pre-commit, the Python-based framework. The difference is in where the hook logic lives. Husky is tied to the Node toolchain and to npm scripts; pre-commit is tied to Python and to its own plugin ecosystem, where each hook is a published repository pinned by revision. Lefthook takes neither route. It is a Go binary with no runtime dependency, and its jobs are shell commands you write yourself, which is why the same lefthook.yml can call yarn, bundle exec and a Docker container in the same hook. The trade-off is that Lefthook does not manage the tools for you: pre-commit downloads and isolates hook environments, while Lefthook assumes the linters are already installed and on PATH. If your team values reproducible, pinned hook environments over direct shell commands, pre-commit's model is a better fit. If you want speed from parallel execution and a single config across languages, Lefthook's model is the simpler one. The README notes that Lefthook is written in Go and can run commands in parallel, which is the concrete difference in approach rather than a matter of taste.

## Maintenance, licensing and upgrade cost

The repository is not archived, and the last push was on 2026-09-21. Releases are frequent: v2.1.14 shipped on 2026-09-14, with v2.1.12 and v2.1.11 in the weeks before. That cadence is relevant because Lefthook is installed per developer, not as a service, so an upgrade is a version bump in package.json, a Gemfile, or a pinned go install path. The README's install examples pin the version explicitly (go install github.com/evilmartians/lefthook/v2@v2.1.14), which is the safer pattern for a tool that writes into .git/hooks. On the licence side, the repository is MIT, which permits commercial use and modification; the practical implication is that you can vendor the binary or fork it if a config feature you need is missing. Note that the MIT licence covers the code, not the Evil Martians name or logo, and I am not giving legal advice. The main upgrade cost is not the binary but the config schema: schema.json is generated by a make target (make jsonschema), so editors that consume it will flag deprecated keys after a version bump. Budget a config review alongside the version change.

## Conclusion

Adopt Lefthook if your repository mixes languages and you want one lefthook.yml to drive pre-commit and pre-push checks without per-ecosystem hook plugins. Skip it if you need documented rollback or a Windows-only workflow, since the README does not cover either. Before committing, run lefthook install in a scratch clone and confirm that lefthook run pre-commit exits non-zero when a job fails.

## FAQ

### What is Lefthook used for?

Lefthook manages Git hooks from a single lefthook.yml file at the repository root. It installs the hooks into the Git project with lefthook install and then runs the configured jobs, such as linters, on commit or push. The README describes it as a Git hooks manager for Node.js, Ruby, Python and other project types.

### How do I install Lefthook?

The README lists several routes: npm install lefthook --save-dev for Node, gem install lefthook for Ruby, pipx install lefthook for Python, and go install github.com/evilmartians/lefthook/v2@v2.1.14 for Go 1.26 or newer. It also points to an installation guide covering apt, brew and winget.

### How do I set up Lefthook in a repository?

Create a lefthook.yml with a hook name such as pre-commit and a jobs list, then run lefthook install to write the hooks into the Git project. The README's quick start shows editing the config, installing, and then committing to trigger the hook.

### What is lefthook.yml?

It is the configuration file Lefthook reads to decide which commands run for each Git hook. It holds hook names, a jobs list with run commands, and optional keys such as glob, exclude, root, tags and parallel. The README links to a separate configuration reference for the full set of options.

### How does Lefthook differ from Husky and pre-commit?

Lefthook is a Go binary with no runtime dependency, and its jobs are shell commands you write, so one config can call yarn, bundle exec or a Docker container. Husky is tied to the Node toolchain, and pre-commit manages pinned hook environments from the Python ecosystem. The README does not publish a comparison page, so the difference is mainly in where the hook logic lives.

### How do I use Lefthook with staged files?

A job's run command can interpolate {staged_files}, which expands to the files currently staged, as shown in the README's lint frontend example. The glob and exclude keys then narrow that list before the command runs.

## Sources

- [evilmartians/lefthook on GitHub](https://github.com/evilmartians/lefthook)
- [License: MIT](https://github.com/evilmartians/lefthook/blob/master/LICENSE)
- [Project website](http://lefthook.dev/)
- [README](https://github.com/evilmartians/lefthook/blob/master/README.md)
- [Releases](https://github.com/evilmartians/lefthook/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/evilmartians-lefthook
