How to set up Lefthook and commitlint in your projects
Learn how to configure Lefthook with commitlint to automate linters, formatters, and commit validation in any JavaScript or TypeScript project.
- Git
- Tooling
Introduction
Lefthook is a significantly faster alternative to Husky, written in Go. It can be installed as a standalone binary or as a Node.js package. In this guide we will use the second option so it is declared as a project dependency and anyone who clones the repository will have it available automatically after installing dependencies.
This guide assumes you already know what git hooks are and what they are used for. Throughout this guide we will configure the pre-commit, commit-msg and pre-push hooks, which are the most commonly used ones, although you will find all available hooks in the official documentation.
You can use any package manager. The examples use pnpm.
Installation
Install the following development dependencies:
pnpm add -DE lefthook@latest @commitlint/cli@latest @commitlint/config-conventional@latestThis installs the latest exact versions, without carets (^), as development dependencies.
Note: If you use pnpm, make sure to update
pnpm-workspace.yaml’sonlyBuiltDependencieswithlefthookand addlefthooktopnpm.onlyBuiltDependenciesin your rootpackage.json, otherwise thepostinstallscript of the lefthook package won’t be executed and hooks won’t be installed.
onlyBuiltDependencies: - lefthookAnd add lefthook to the onlyBuiltDependencies section in your root package.json:
{ ... "pnpm": { "onlyBuiltDependencies": [ "lefthook" ] }}Commitlint configuration
Commitlint validates that commit messages follow the Conventional Commits standard, making it easier to generate automatic changelogs and maintain a readable history.
Create the configuration file at the root of your project:
export default { extends: ["@commitlint/config-conventional"] }Lefthook configuration
For this example I will assume the project uses Biome as linter and formatter, with the following scripts in package.json:
{ ... "scripts": { "format": "biome format", "lint": "biome lint", "check": "biome check", }}Create the lefthook.yml file at the root of your project:
# Run linters and formatters on staged files before committingpre-commit: parallel: false # run all commands concurrently commands: check: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" run: pnpm run check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true
# Validate commit messagescommit-msg: commands: commitlint: run: pnpm commitlint --edit {1}
# Check formatting and lint before pushingpre-push: commands: check: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" run: npx @biomejs/biome check --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {push_files}Each section maps to a Git hook:
pre-commit— runs on staged files before the commit is created. Thestage_fixed: trueoption automatically re-stages any files that Biome has corrected.commit-msg— validates the commit message with commitlint before it is recorded.pre-push— checks all files modified in the push without applying automatic fixes.
The parallel option controls whether the commands within a hook run in parallel or sequentially. Since pre-commit only has one command in this example, the value false (which is the default) makes no difference, but I include it explicitly so it is clear if you add more commands later.
You can extend this configuration to fit your project’s needs: unit tests in pre-commit, end-to-end tests in pre-push, type generation, and so on. Check the official Lefthook documentation for all available options.