Introduction

Lefthook is a Git hooks manager. This documentation provides the reference for installing, configuring and using the lefthook.

Maybe you are looking for


Sponsored by Evil Martians

❓If you have a question or found a mistake in the documentation, please create a new discussion. Small contributions help maintaining the quality of the project.

Install lefthook

Choose your fighter:

Ruby

gem install lefthook

Troubleshooting

If you see the error lefthook: command not found you need to check your $PATH. Also try to restart your terminal.

Node.js

Lefthook is available on NPM in the following flavors:

  1. lefthook that will install the proper binary:

    npm install lefthook --save-dev
    # or yarn:
    yarn add -D lefthook
    
  2. @evilmartians/lefthook with pre-bundled binaries for all architectures:

    npm install @evilmartians/lefthook --save-dev
    # or yarn:
    yarn add -D @evilmartians/lefthook
    
  3. @evilmartians/lefthook-installer that will fetch binary file on installation:

    npm install @evilmartians/lefthook-installer --save-dev
    # or yarn:
    yarn add -D @evilmartians/lefthook-installer
    

Note: If you use pnpm package manager make sure you set side-effects-cache = false in your .npmrc, otherwise the postinstall script of the lefthook package won't be executed and hooks won't be installed.

Go

go install github.com/evilmartians/lefthook@latest

Python

python -m pip install --user lefthook

Swift

You can find the Swift wrapper plugin here.

Utilize lefthook in your Swift project using Swift Package Manager:

.package(url: "https://github.com/csjones/lefthook-plugin.git", exact: "1.9.3"),

Or, with mint:

mint run csjones/lefthook-plugin

Scoop for Windows

scoop install lefthook

Homebrew for MacOS and Linux

brew install lefthook

Winget for Windows

winget install evilmartians.lefthook

Snap for Linux

snap install --classic lefthook

APT packages for Debian/Ubuntu Linux

curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.deb.sh' | sudo -E bash
sudo apt install lefthook

See all instructions: https://cloudsmith.io/~evilmartians/repos/lefthook/setup/#formats-deb

Hosted By: Cloudsmith

RPM packages for CentOS/Fedora Linux

curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.rpm.sh' | sudo -E bash
sudo yum install lefthook

See all instructions: https://cloudsmith.io/~evilmartians/repos/lefthook/setup/#repository-setup-yum

Hosted By: Cloudsmith

APK packages for Alpine

sudo apk add --no-cache bash curl
curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.alpine.sh' | sudo -E bash
sudo apk add lefthook

See all instructions: https://cloudsmith.io/~evilmartians/repos/lefthook/setup/#formats-alpine

Hosted By: Cloudsmith

AUR for Arch

You can install lefthook package from AUR.

yay -S lefthook

Manuall installation with prebuilt executable

Or take it from binaries and install manually.

  1. Download the executable for your OS and Arch
  2. Put the executable under the $PATH (for unix systems)

Usage

Commands

tip

Use lefthook help or lefthook <command> -h/--help to discover available commands and their options

lefthook install

lefthook install creates an empty lefthook.yml if a configuration file does not exist and updates git hooks to use lefthook. Run lefthook install after cloning the git repo.

note

NPM package lefthook installs the hooks in a postinstall script automatically.

lefthook uninstall

lefthook uninstall clears git hooks affected by lefthook. If you have lefthook installed as an NPM package you should remove it manually.

lefthook add

lefthook add pre-commit will create a file .git/hooks/pre-commit. This is the same lefthook does for install command but you don't need to create a configuration first.

To use custom scripts as hooks create the required directories with lefthook add pre-commit --dirs.

Example

$ lefthook add pre-push --dirs

Describe pre-push commands in lefthook.yml:

pre-push:
  scripts:
    "audit.sh":
      runner: bash

Edit the script:

$ vim .lefthook/pre-push/audit.sh
...

Run git push and lefthook will run bash audit.sh as a pre-push hook.

lefthook run

lefthook run executes the commands and scripts configured for a given hook. Generated hooks call lefthook run implicitly.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint --fix

test:
  commands:
    js-test:
      run: yarn test

Install the hook.

$ lefthook install

Run test.

$ lefthook run test # will run 'yarn test'

Commit changes.

$ git commit # will run pre-commit hook ('yarn lint --fix')

Or run manually also

$ lefthook run pre-commit

You can also specify a flag to run only some commands:

$ lefthook run pre-commit --commands lint

and optionally run either on all files (any {staged_files} placeholder acts as {all_files}) or a list of files:

$ lefthook run pre-commit --all-files
$ lefthook run pre-commit --file file1.js --file file2.js

(if both are specified, --all-files is ignored)

lefthook version

lefthook version prints the current binary version. Print the commit hash with lefthook version --full

Example

$ lefthook version --full

1.1.3 bb099d13c24114d2859815d9d23671a32932ffe2

lefthook self-update

lefthook self-update updates the binary with the latest lefthook release on Github. This command is available only if you install lefthook from sources or download the binary from the Github Releases. For other ways use package-specific commands to update lefthook.

ENV variables

ENV variables can be used to control lefthook behavior. Most of them have the alternative CLI or config options.

LEFTHOOK

Use LEFTHOOK=0 git ... or LEFTHOOK=false git ... to disable lefthook when running git commands.

Example

LEFTHOOK=0 git commit -am "Lefthook skipped"

LEFTHOOK_EXCLUDE

Use LEFTHOOK_EXCLUDE={list of tags or command names to be excluded} to skip some commands or scripts by tag or name (for commands only). See the exclude_tags configuration option for more details.

Example

LEFTHOOK_EXCLUDE=ruby,security,lint git commit -am "Skip some tag checks"

LEFTHOOK_OUTPUT

Use LEFTHOOK_OUTPUT={list of output values} to specify what to print in your output. You can also set LEFTHOOK_OUTPUT=false to disable all output except for errors. Refer to the output configuration option for more details.

Example

$ LEFTHOOK_OUTPUT=summary lefthook run pre-commit
summary: (done in 0.52 seconds)
✔️  lint

LEFTHOOK_QUIET

You can skip some outputs printed by lefthook by setting the LEFTHOOK_QUIET environment variable. Provide a list of output types to be skipped. See the skip_output configuration option for more details.

Example

$ LEFTHOOK_QUIET=meta,execution lefthook run pre-commit

  EXECUTE > lint

SUMMARY: (done in 0.01 seconds)
🥊  lint

LEFTHOOK_VERBOSE

Set LEFTHOOK_VERBOSE=1 or LEFTHOOK_VERBOSE=true to enable verbose printing.

LEFTHOOK_BIN

Set LEFTHOOK_BIN to a location where lefthook is installed to use that instead of trying to detect from the it the PATH or from a package manager.

Useful for cases when:

  • lefthook is installed multiple ways, and you want to be explicit about which one is used (example: installed through homebrew, but also is in Gemfile but you are using a ruby version manager like rbenv that prepends it to the path)
  • debugging and/or developing lefthook

NO_COLOR

Set NO_COLOR=true to disable colored output in lefthook and all subcommands that lefthook calls.

CLICOLOR_FORCE

Set CLICOLOR_FORCE=true to force colored output in lefthook and all subcommands.

Tips

Small tips for better experience with lefthook

Local config

Use lefthook-local.yml to overwrite or extend options from the main config. (Don't forget to add this file to .gitignore)

Disable lefthook in CI

When using NPM package lefthook set CI=true in your CI (if it does not set automatically). When CI=true is set lefthook NPM package won't install the hooks in the postinstall script.

Commitlint example

Let's create a bash script to check conventional commit status .lefthook/commit-msg/commitlint.sh:

echo $(head -n1 $1) | npx commitlint --color

Now we can ask lefthook to run our bash script by adding this code to lefthook.yml file:

# lefthook.yml

commit-msg:
  scripts:
    "commitlint.sh":
      runner: bash

When you try to commit git commit -m "haha bad commit text" script commitlint.sh will be executed. Since commit text doesn't match the default config or custom config that you setup for commitlint, the process will be interrupted.

Parallel execution

You can enable parallel execution if you want to speed up your checks. Lets imagine we have the following rules to lint the whole project:

bundle exec rubocop --parallel && \
bundle exec danger && \
yarn eslint --ext .es6 app/assets/javascripts && \
yarn eslint --ext .es6 test/javascripts && \
yarn eslint --ext .es6 plugins/**/assets/javascripts && \
yarn eslint --ext .es6 plugins/**/test/javascripts && \
yarn eslint app/assets/javascripts test/javascripts

Rewrite it in lefthook custom group. We call it lint:

# lefthook.yml

lint:
  parallel: true
  commands:
    rubocop:
      run: bundle exec rubocop --parallel
    danger:
      run: bundle exec danger
    eslint-assets:
      run: yarn eslint --ext .es6 app/assets/javascripts
    eslint-test:
      run: yarn eslint --ext .es6 test/javascripts
    eslint-plugins-assets:
      run: yarn eslint --ext .es6 plugins/**/assets/javascripts
    eslint-plugins-test:
      run: yarn eslint --ext .es6 plugins/**/test/javascripts
    eslint-assets-tests:
      run: yarn eslint app/assets/javascripts test/javascripts

Then call this group directly:

lefthook run lint

Concurrent files overrides

To prevent concurrent problems with read/write files try flock utility.

# lefthook.yml

graphql-schema:
  glob: "{Gemfile.lock,app/graphql/**/*}"
  run: flock webpack/application/typings/graphql-schema.json yarn typings:update && git diff --exit-code --stat HEAD webpack/application/typings
frontend-tests:
  glob: "**/*.js"
  run: flock -s webpack/application/typings/graphql-schema.json yarn test --findRelatedTests {files}
frontend-typings:
  glob: "**/*.js"
  run: flock -s webpack/application/typings/graphql-schema.json yarn run flow focus-check {files}

Capture ARGS from git in the script

Example script for prepare-commit-msg hook:

COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3

# ...

Git LFS support

If git-lfs binary is not installed and not required in your project, LFS hooks won't be executed, and you won't be warned about it.

Lefthook runs LFS hooks internally for the following hooks:

  • post-checkout
  • post-commit
  • post-merge
  • pre-push

Errors are suppressed if git LFS is not required for the project. You can use LEFTHOOK_VERBOSE ENV to make lefthook show git LFS output.

Pass stdin to a command or script

When you need to read the data from stdin – specify use_stdin: true. This option is good when you write a command or script that receives data from git using stdin (for the pre-push hook, for example).

Using an interactive command or script

When you need to interact with user – specify interactive: true. Lefthook will connect to the current TTY and forward it to your command's or script's stdin.

Config file name

Lefthook supports the following file names for the main config:

  • lefthook.yml
  • .lefthook.yml
  • lefthook.yaml
  • .lefthook.yaml
  • lefthook.toml
  • .lefthook.toml
  • lefthook.json
  • .lefthook.json

If there are more than 1 file in the project, only one will be used, and you'll never know which one. So, please, use one format in a project.

Lefthook also merges an extra config with the name lefthook-local. All supported formats can be applied to this -local config. If you name your main config with the leading dot, like .lefthook.json, the -local config also must be named with the leading dot: .lefthook-local.json.

Options

assert_lefthook_installed

Default: false

When set to true, fail (with exit status 1) if lefthook executable can't be found in $PATH, under node_modules/, as a Ruby gem, or other supported method. This makes sure git hook won't omit lefthook rules if lefthook ever was installed.

colors

Default: auto

Whether enable or disable colorful output of Lefthook. This option can be overwritten with --colors option. You can also provide your own color codes.

Example

Disable colors.

# lefthook.yml

colors: false

Custom color codes. Can be hex or ANSI codes.

# lefthook.yml

colors:
  cyan: 14
  gray: 244
  green: '#32CD32'
  red: '#FF1493'
  yellow: '#F0E68C'

no_tty

Default: false

Whether hide spinner and other interactive things. This can be also controlled with --no-tty option for lefthook run command.

Example

# lefthook.yml

no_tty: true

extends

You can extend your config with another one YAML file. Its content will be merged. Extends for lefthook.yml, lefthook-local.yml, and remotes configs are handled separately, so you can have different extends in these files.

You can use asterisk to make a glob.

Example

# lefthook.yml

extends:
  - /home/user/work/lefthook-extend.yml
  - /home/user/work/lefthook-extend-2.yml
  - lefthook-extends/file.yml
  - ../extend.yml
  - projects/*/specific-lefthook-config.yml

The extends will be merged to the main configuration in your file. Here is the order of settings applied:

  • lefthook.yml – main config file
  • extends – configs specified in extends option
  • remotes – configs specified in remotes option
  • lefthook-local.yml – local config file

So, extends override settings from lefthook.yml, remotes override extends, and lefthook-local.yml can override everything.

min_version

If you want to specify a minimum version for lefthook binary (e.g. if you need some features older versions don't have) you can set this option.

Example

# lefthook.yml

min_version: 1.1.3

output

You can manage verbosity using the output config. You can specify what to print in your output by setting these values, which you need to have

Possible values are meta,summary,success,failure,execution,execution_out,execution_info,skips. By default, all output values are enabled

You can also disable all output with setting output: false. In this case only errors will be printed.

This config quiets all outputs except for errors.

output is enabled if there is no skip_output and LEFTHOOK_QUIET.

Example

# lefthook.yml

output:
  - meta           # Print lefthook version
  - summary        # Print summary block (successful and failed steps)
  - empty_summary  # Print summary heading when there are no steps to run
  - success        # Print successful steps
  - failure        # Print failed steps printing
  - execution      # Print any execution logs
  - execution_out  # Print execution output
  - execution_info # Print `EXECUTE > ...` logging
  - skips          # Print "skip" (i.e. no files matched)

You can also extend this list with an environment variable LEFTHOOK_OUTPUT:

LEFTHOOK_OUTPUT="meta,success,summary" lefthook run pre-commit

skip_output

DEPRECATED This feature is deprecated and might be removed in future versions. Please, use output instead for managing verbosity.

You can manage the verbosity using the skip_output config. You can set whether lefthook should print some parts of its output.

Possible values are meta,summary,success,failure,execution,execution_out,execution_info,skips.

You can also disable all output with setting skip_output: true. In this case only errors will be printed.

This config quiets all outputs except for errors.

Example

# lefthook.yml

skip_output:
  - meta           # Skips lefthook version printing
  - summary        # Skips summary block (successful and failed steps) printing
  - empty_summary  # Skips summary heading when there are no steps to run
  - success        # Skips successful steps printing
  - failure        # Skips failed steps printing
  - execution      # Skips printing any execution logs (but prints if the execution failed)
  - execution_out  # Skips printing execution output (but still prints failed commands output)
  - execution_info # Skips printing `EXECUTE > ...` logging
  - skips          # Skips "skip" printing (i.e. no files matched)

You can also extend this list with an environment variable LEFTHOOK_QUIET:

LEFTHOOK_QUIET="meta,success,summary" lefthook run pre-commit

source_dir

Default: .lefthook/

Change a directory for script files. Directory for script files contains folders with git hook names which contain script files.

Example of directory tree:

.lefthook/
├── pre-commit/
│   ├── lint.sh
│   └── test.py
└── pre-push/
    └── check-files.rb

source_dir_local

Default: .lefthook-local/

Change a directory for local script files (not stored in VCS).

This option is useful if you have a lefthook-local.yml config file and want to reference different scripts there.

rc

Provide an rc file, which is actually a simple sh script. Currently it can be used to set ENV variables that are not accessible from non-shell programs.

Example

Use cases:

  • You have a GUI program that runs git hooks (e.g., VSCode)
  • You reference executables that are accessible only from a tweaked $PATH environment variable (e.g., when using rbenv or nvm, fnm)
  • Or even if your GUI program cannot locate the lefthook executable :scream:
  • Or if you want to use ENV variables that control the executables behavior in lefthook.yml
# An npm executable which is managed by nvm
$ which npm
/home/user/.nvm/versions/node/v15.14.0/bin/npm
# lefthook.yml

pre-commit:
  commands:
    lint:
      run: npm run eslint {staged_files}

Provide a tweak to access npm executable the same way you do it in your ~/rc.

# lefthook-local.yml

# You can choose whatever name you want.
# You can share it between projects where you use lefthook.
# Make sure the path is absolute.
rc: ~/.lefthookrc

Or

# lefthook-local.yml

# If the path contains spaces, you need to quote it.
rc: '"${XDG_CONFIG_HOME:-$HOME/.config}/lefthookrc"'

In the rc file, export any new environment variables or modify existing ones.

# ~/.lefthookrc

# An nvm way
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# An fnm way
export FNM_DIR="$HOME/.fnm"
[ -s "$FNM_DIR/fnm.sh" ] && \. "$FNM_DIR/fnm.sh"

# Or maybe just
PATH=$PATH:$HOME/.nvm/versions/node/v15.14.0/bin
# Make sure you updated git hooks. This is important.
$ lefthook install -f

Now any program that runs your hooks will have a tweaked PATH environment variable and will be able to get nvm :wink:

remotes

You can provide multiple remote configs if you want to share yours lefthook configurations across many projects. Lefthook will automatically download and merge configurations into your local lefthook.yml.

You can use extends but the paths must be relative to the remote repository root.

If you provide scripts in a remote config file, the scripts folder must also be in the root of the repository.

Note

The configuration from remotes will be merged to the local config using the following priority:

  1. Local main config (lefthook.yml)
  2. Remote configs (remotes)
  3. Local overrides (lefthook-local.yml)

This priority may be changed in the future. For convenience, if you use remotes, please don't configure any hooks.

git_url

A URL to Git repository. It will be accessed with privileges of the machine lefthook runs on.

Example

# lefthook.yml

remotes:
  - git_url: git@github.com:evilmartians/lefthook

Or

# lefthook.yml

remotes:
  - git_url: https://github.com/evilmartians/lefthook

ref

An optional branch or tag name.

Note: If you initially had ref option, ran lefthook install, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups.

Example

# lefthook.yml

remotes:
  - git_url: git@github.com:evilmartians/lefthook
    ref: v1.0.0

refetch

Default: false

Force remote config refetching on every run. Lefthook will be refetching the specified remote every time it is called.

Example

# lefthook.yml

remotes:
  - git_url: https://github.com/evilmartians/lefthook
    refetch: true

refetch_frequency

Default: Not set

Specifies how frequently Lefthook should refetch the remote configuration. This can be set to always, never or a time duration like 24h, 30m, etc.

  • When set to always, Lefthook will always refetch the remote configuration on each run.
  • When set to a duration (e.g., 24h), Lefthook will check the last fetch time and refetch the configuration only if the specified amount of time has passed.
  • When set to never or not set, Lefthook will not fetch from remote.

Example

# lefthook.yml

remotes:
  - git_url: https://github.com/evilmartians/lefthook
    refetch_frequency: 24h # Refetches once every 24 hours

WARNING If refetch is set to true, it overrides any setting in refetch_frequency.

configs

Default: [lefthook.yml]

An optional array of config paths from remote's root.

Example

# lefthook.yml

remotes:
  - git_url: git@github.com:evilmartians/lefthook
    ref: v1.0.0
    configs:
      - examples/ruby-linter.yml
      - examples/test.yml

Example with multiple remotes merging multiple configurations.

# lefthook.yml

remotes:
  - git_url: git@github.com:org/lefthook-configs
    ref: v1.0.0
    configs:
      - examples/ruby-linter.yml
      - examples/test.yml
  - git_url: https://github.com/org2/lefthook-configs
    configs:
      - lefthooks/pre_commit.yml
      - lefthooks/post_merge.yml
  - git_url: https://github.com/org3/lefthook-configs
    ref: feature/new
    configs:
      - configs/pre-push.yml

Git hook

Contains settings for the git hook (commands, scripts, skip rules, etc.). You can specify any Git hook or your own custom, e.g. test

Hook options

files (global)

A custom git command for files to be referenced in {files} template. See run and files.

If the result of this command is empty, the execution of commands will be skipped.

Example

# lefthook.yml

pre-commit:
  files: git diff --name-only master # custom list of files
  commands:
    ...

parallel

Default: false

Note: Lefthook runs commands and scripts sequentially by default

Run commands and scripts concurrently.

piped

Default: false

Note: Lefthook will return an error if both piped: true and parallel: true are set

Stop running commands and scripts if one of them fail.

Example

# lefthook.yml

database:
  piped: true # Stop if one of the steps fail
  commands:
    1_create:
      run: rake db:create
    2_migrate:
      run: rake db:migrate
    3_seed:
      run: rake db:seed

follow

Default: false

Follow the STDOUT of the running commands and scripts.

Example

# lefthook.yml

pre-push:
  follow: true
  commands:
    backend-tests:
      run: bundle exec rspec
    frontend-tests:
      run: yarn test

Note: If used with parallel the output can be a mess, so please avoid setting both options to true

exclude_tags

Tags or command names that you want to exclude. This option can be overwritten with LEFTHOOK_EXCLUDE env variable.

Example

# lefthook.yml

pre-commit:
  exclude_tags: frontend
  commands:
    lint:
      tags: frontend
      ...
    test:
      tags: frontend
      ...
    check-syntax:
      tags: documentation
lefthook run pre-commit # will only run check-syntax command

Notes

This option is good to specify in lefthook-local.yml when you want to skip some execution locally.

# lefthook.yml

pre-push:
  commands:
    packages-audit:
      tags:
        - frontend
        - security
      run: yarn audit
    gems-audit:
      tags:
        - backend
        - security
      run: bundle audit

You can skip commands by tags:

# lefthook-local.yml

pre-push:
  exclude_tags:
    - frontend

skip

You can skip all or specific commands and scripts using skip option. You can also skip when merging, rebasing, or being on a specific branch. Globs are available for branches.

Possible skip values:

  • rebase - when in rebase git state
  • merge - when in merge git state
  • merge-commit - when current HEAD commit is the merge commit
  • ref: main - when on a main branch
  • run: test ${SKIP_ME} -eq 1 - when test ${SKIP_ME} -eq 1 is successful (return code is 0)

Example

Always skipping a command:

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: true
      run: yarn lint

Skipping on merging and rebasing:

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip:
        - merge
        - rebase
      run: yarn lint

Or

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: merge
      run: yarn lint

Skipping when your are on a merge commit:

# lefthook.yml

pre-push:
  commands:
    lint:
      skip: merge-commit
      run: yarn lint

Skipping the whole hook on main branch:

# lefthook.yml

pre-commit:
  skip:
    - ref: main
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

Skipping hook for all dev/* branches:

# lefthook.yml

pre-commit:
  skip:
    - ref: dev/*
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

Skipping hook by running a command:

# lefthook.yml

pre-commit:
  skip:
    - run: test "${NO_HOOK}" -eq 1
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

TIP

Always skipping is useful when you have a lefthook-local.yml config and you don't want to run some commands locally. So you just overwrite the skip option for them to be true.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint
# lefthook-local.yml

pre-commit:
  commands:
    lint:
      skip: true

only

You can force a command, script, or the whole hook to execute only in certain conditions. This option acts like the opposite of skip. It accepts the same values but skips execution only if the condition is not satisfied.

Note: skip option takes precedence over only option, so if you have conflicting conditions the execution will be skipped.

Example

Execute a hook only for dev/* branches.

# lefthook.yml

pre-commit:
  only:
    - ref: dev/*
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

When rebasing execute quick linter but skip usual linter and tests.

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: rebase
      run: yarn lint
    test:
      skip: rebase
      run: yarn test
    lint-on-rebase:
      only: rebase
      run: yarn lint-quickly

commands

Commands to be executed for the hook. Each command has a name and associated run options.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      ... # command options

Command options

run

This is a mandatory option for a command. This is actually a command that is executed for the hook.

You can use files templates that will be substituted with the appropriate files on execution:

  • {files} - custom files command result.
  • {staged_files} - staged files which you try to commit.
  • {push_files} - files that are committed but not pushed.
  • {all_files} - all files tracked by git.
  • {cmd} - shorthand for the command from lefthook.yml.
  • {0} - shorthand for the single space-joint string of git hook arguments.
  • {N} - shorthand for the N-th git hook argument.

Example

Run yarn lint on pre-commit hook.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint

{files} template

Run go vet only on files listed with git ls-files -m command with .go extension.

# lefthook.yml

pre-commit:
  commands:
    govet:
      files: git ls-files -m
      glob: "*.go"
      run: go vet {files}

{staged_files} template

Run yarn eslint only on staged files with .js, .ts, .jsx, and .tsx extensions.

# lefthook.yml

pre-commit:
  commands:
    eslint:
      glob: "*.{js,ts,jsx,tsx}"
      run: yarn eslint {staged_files}

{push_files} template

If you want to lint files only before pushing them.

# lefthook.yml

pre-push:
  commands:
    eslint:
      glob: "*.{js,ts,jsx,tsx}"
      run: yarn eslint {push_files}

{all_files} template

Simply run bundle exec rubocop on all files with .rb extension excluding application.rb and routes.rb files.

Note: --force-exclusion will apply Exclude configuration setting of Rubocop

# lefthook.yml

pre-commit:
  commands:
    rubocop:
      tags:
        - backend
        - style
      glob: "*.rb"
      exclude:
        - config/application.rb
        - config/routes.rb
      run: bundle exec rubocop --force-exclusion {all_files}

{cmd} template

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint
  scripts:
    "good_job.js":
      runner: node

You can wrap it in docker runner locally:

# lefthook-local.yml

pre-commit:
  commands:
    lint:
      run: docker run -it --rm <container_id_or_name> {cmd}
  scripts:
    "good_job.js":
      runner: docker run -it --rm <container_id_or_name> {cmd}

Git arguments

Make sure commits are signed.

# lefthook.yml

# Note: commit-msg hook takes a single parameter,
#       the name of the file that holds the proposed commit log message.
# Source: https://git-scm.com/docs/githooks#_commit_msg
commit-msg:
  commands:
    multiple-sign-off:
      run: 'test $(grep -c "^Signed-off-by: " {1}) -lt 2'

Rubocop

If using {all_files} with RuboCop, it will ignore RuboCop's Exclude configuration setting. To avoid this, pass --force-exclusion.

Quotes

If you want to have all your files quoted with double quotes " or single quotes ', quote the appropriate shorthand:

# lefthook.yml

pre-commit:
  commands:
    lint:
      glob: "*.js"
      # Quoting with double quotes `"` might be helpful for Windows users
      run: yarn eslint "{staged_files}" # will run `yarn eslint "file1.js" "file2.js" "[strange name].js"`
    test:
      glob: "*.{spec.js}"
      run: yarn test '{staged_files}' # will run `yarn eslint 'file1.spec.js' 'file2.spec.js' '[strange name].spec.js'`
    format:
      glob: "*.js"
      # Will quote where needed with single quotes
      run: yarn test {staged_files} # will run `yarn eslint file1.js file2.js '[strange name].spec.js'`

skip

You can skip all or specific commands and scripts using skip option. You can also skip when merging, rebasing, or being on a specific branch. Globs are available for branches.

Possible skip values:

  • rebase - when in rebase git state
  • merge - when in merge git state
  • merge-commit - when current HEAD commit is the merge commit
  • ref: main - when on a main branch
  • run: test ${SKIP_ME} -eq 1 - when test ${SKIP_ME} -eq 1 is successful (return code is 0)

Example

Always skipping a command:

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: true
      run: yarn lint

Skipping on merging and rebasing:

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip:
        - merge
        - rebase
      run: yarn lint

Or

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: merge
      run: yarn lint

Skipping when your are on a merge commit:

# lefthook.yml

pre-push:
  commands:
    lint:
      skip: merge-commit
      run: yarn lint

Skipping the whole hook on main branch:

# lefthook.yml

pre-commit:
  skip:
    - ref: main
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

Skipping hook for all dev/* branches:

# lefthook.yml

pre-commit:
  skip:
    - ref: dev/*
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

Skipping hook by running a command:

# lefthook.yml

pre-commit:
  skip:
    - run: test "${NO_HOOK}" -eq 1
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

TIP

Always skipping is useful when you have a lefthook-local.yml config and you don't want to run some commands locally. So you just overwrite the skip option for them to be true.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint
# lefthook-local.yml

pre-commit:
  commands:
    lint:
      skip: true

only

You can force a command, script, or the whole hook to execute only in certain conditions. This option acts like the opposite of skip. It accepts the same values but skips execution only if the condition is not satisfied.

Note: skip option takes precedence over only option, so if you have conflicting conditions the execution will be skipped.

Example

Execute a hook only for dev/* branches.

# lefthook.yml

pre-commit:
  only:
    - ref: dev/*
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

When rebasing execute quick linter but skip usual linter and tests.

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: rebase
      run: yarn lint
    test:
      skip: rebase
      run: yarn test
    lint-on-rebase:
      only: rebase
      run: yarn lint-quickly

tags

You can specify tags for commands and scripts. This is useful for excluding. You can specify more than one tag using comma.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      tags:
        - frontend
        - js
      run: yarn lint
    test:
      tags:
        - backend
        - ruby
      run: bundle exec rspec

glob

You can set a glob to filter files for your command. This is only used if you use a file template in run option or provide your custom files command.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      glob: "*.{js,ts,jsx,tsx}"
      run: yarn eslint {staged_files}

Notes

For patterns that you can use see this reference. We use glob library.

If you've specified glob but don't have a files template in run option, lefthook will check {staged_files} for pre-commit hook and {push_files} for pre-push hook and apply filtering. If no files left, the command will be skipped.

# lefthook.yml

pre-commit:
  commands:
    lint:
      glob: "*.js"
      run: npm run lint # skipped if no .js files staged

files

A custom git command for files or directories to be referenced in {files} template for run setting.

If the result of this command is empty, the execution of commands will be skipped.

This option overwrites the hook-level files option.

Example

Provide a git command to list files.

# lefthook.yml

pre-push:
  commands:
    stylelint:
      tags:
        - frontend
        - style
      files: git diff --name-only master
      glob: "*.js"
      run: yarn stylelint {files}

Call a custom script for listing files.

# lefthook.yml

pre-push:
  commands:
    rubocop:
      tags: backend
      glob: "**/*.rb"
      files: node ./lefthook-scripts/ls-files.js # you can call your own scripts
      run: bundle exec rubocop --force-exclusion --parallel {files}

file_types

Filter files in a run templates by their type. Supported types:

File typeExlanation
textAny file that contains text. Symlinks are not followed.
binaryAny file that contains non-text bytes. Symlinks are not followed.
executableAny file that has executable bits set. Symlinks are not followed.
not executableAny file without executable bits in file mode. Symlinks included.
symlinkA symlink file.
not symlinkAny non-symlink file.

Important: When passed multiple file types all constraints will be applied to the resulting list of files

Examples

Apply some different linters on text and binary files.

# lefthook.yml

pre-commit:
  commands:
    lint-code:
      run: yarn lint {staged_files}
      file_types: text
    check-hex-codes:
      run: yarn check-hex {staged_files}
      file_types: binary

Skip symlinks.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint --fix {staged_files}
      file_types:
        - not symlink

Lint executable scripts.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint --fix {staged_files}
      file_types:
        - executable
        - text

env

You can specify some ENV variables for the command or script.

Example

# lefthook.yml

pre-commit:
  commands:
    test:
      env:
        RAILS_ENV: test
      run: bundle exec rspec

Extending PATH

If your hook is run by GUI program, and you use some PATH tweaks in your ~/.rc, you might see an error saying executable not found. In that case You can extend the $PATH variable with lefthook-local.yml configuration the following way.

# lefthook.yml

pre-commit:
  commands:
    test:
      run: yarn test
# lefthook-local.yml

pre-commit:
  commands:
    test:
      env:
        PATH: $PATH:/home/me/path/to/yarn

Notes

This option is useful when using lefthook on different OSes or shells where ENV variables are set in different ways.

root

You can change the CWD for the command you execute using root option.

This is useful when you execute some npm or yarn command but the package.json is in another directory.

For pre-push and pre-commit hooks and for the custom files command root option is used to filter file paths. If all files are filtered the command will be skipped.

Example

Format and stage files from a client/ folder.

# Folders structure

$ tree .
.
├── client/
│   ├── package.json
│   ├── node_modules/
|   ├── ...
├── server/
|   ...
# lefthook.yml

pre-commit:
  commands:
    lint:
      root: "client/"
      glob: "*.{js,ts}"
      run: yarn eslint --fix {staged_files} && git add {staged_files}

exclude

For the exclude option two variants are supported:

  • A list of globs to be excluded
  • A single regular expression (deprecated)

Note: The regular expression is matched against full paths to files in the repo, relative to the repo root, using / as the directory separator on all platforms. File paths do not begin with the separator or any other prefix.

Example

Run Rubocop on staged files with .rb extension except for application.rb, routes.rb, rails_helper.rb, and all Ruby files in config/initializers/.

# lefthook.yml

pre-commit:
  commands:
    lint:
      glob: "*.rb"
      exclude:
        - config/routes.rb
        - config/application.rb
        - config/initializers/*.rb
        - spec/rails_helper.rb
      run: bundle exec rubocop --force-exclusion {staged_files}

The same example using a regular expression.

# lefthook.yml

pre-commit:
  commands:
    lint:
      glob: "*.rb"
      exclude: '(^|/)(application|routes|rails_helper|initializers/\w+)\.rb$'
      run: bundle exec rubocop --force-exclusion {staged_files}

Important

Be careful with the config file format's string quoting and escaping rules when writing regexps in it. For YAML, single quotes are often the simplest choice.

If you've specified exclude but don't have a files template in run option, lefthook will check {staged_files} for pre-commit hook and {push_files} for pre-push hook and apply filtering. If no files left, the command will be skipped.

# lefthook.yml

pre-commit:
  commands:
    lint:
      exclude: '(^|/)application\.rb$'
      run: bundle exec rubocop # skipped if only application.rb was staged

fail_text

You can specify a text to show when the command or script fails.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint
      fail_text: Add node executable to $PATH
$ git commit -m 'fix: Some bug'

Lefthook v1.1.3
RUNNING HOOK: pre-commit

  EXECUTE > lint

SUMMARY: (done in 0.01 seconds)
🥊  lint: Add node executable to $PATH env

stage_fixed

Default: false

Works only for pre-commit hook

When set to true lefthook will automatically call git add on files after running the command or script. For a command if files option was specified, the specified command will be used to retrieve files for git add. For scripts and commands without files option {staged_files} template will be used. All filters (glob, exclude) will be applied if specified.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: npm run lint --fix {staged_files}
      stage_fixed: true

interactive

Default: false

Note: If you want to pass stdin to your command or script but don't need to get the input from CLI, use use_stdin option instead.

Whether to use interactive mode. This applies the certain behavior:

  • All interactive commands/scripts are executed after non-interactive. Exception: piped option is set to true.
  • When executing, lefthook tries to open /dev/tty (Linux/Unix only) and use it as stdin.
  • When no_tty option is set, interactive is ignored.

use_stdin

Note: With many commands or scripts having use_stdin: true, only one will receive the data. The others will have nothing. If you need to pass the data from stdin to every command or script, please, submit a feature request.

Pass the stdin from the OS to the command/script.

Example

Use this option for the pre-push hook when you have a script that does while read .... Without this option lefthook will hang: lefthook uses pseudo TTY by default, and it doesn't close stdin when all data is read.

# .lefthook/pre-push/do-the-magic.sh

remote="$1"
url="$2"

while read local_ref local_oid remote_ref remote_oid; do
  # ...
done
# lefthook.yml
pre-push:
  scripts:
    "do-the-magic.sh":
      runner: bash
      use_stdin: true

priority

Default: 0

Note: This option makes sense only when parallel: false or piped: true is set.

Value 0 is considered an +Infinity, so commands or scripts with priority: 0 or without this setting will be run at the very end.

Set priority from 1 to +Infinity. This option can be used to configure the order of the sequential steps.

Example

# lefthook.yml

post-checkout:
  piped: true
  commands:
    db-create:
      priority: 1
      run: rails db:create
    db-migrate:
      priority: 2
      run: rails db:migrate
    db-seed:
      priority: 3
      run: rails db:seed

  scripts:
    "check-spelling.sh":
      runner: bash
      priority: 1
    "check-grammar.rb":
      runner: ruby
      priority: 2

Scripts

Scripts are stored under <source_dir>/<hook-name>/ folder. These scripts are your own executables which are being run in the project root.

To add a script for a pre-commit hook:

  1. Run lefthook add -d pre-commit
  2. Edit .lefthook/pre-commit/my-script.sh
  3. Add an entry to lefthook.yml
    # lefthook.yml
    
    pre-commit:
      scripts:
        "my-script.sh":
          runner: bash
    

Example

Let's create a bash script to check commit templates .lefthook/commit-msg/template_checker:

INPUT_FILE=$1
START_LINE=`head -n1 $INPUT_FILE`
PATTERN="^(TICKET)-[[:digit:]]+: "
if ! [[ "$START_LINE" =~ $PATTERN ]]; then
  echo "Bad commit message, see example: TICKET-123: some text"
  exit 1
fi

Now we can ask lefthook to run our bash script by adding this code to lefthook.yml file:

# lefthook.yml

commit-msg:
  scripts:
    "template_checker":
      runner: bash

When you try to commit git commit -m "bad commit text" script template_checker will be executed. Since commit text doesn't match the described pattern the commit process will be interrupted.

Script options

runner

You should specify a runner for the script. This is a command that should execute a script file. It will be called the following way: <runner> <path-to-script> (e.g. ruby .lefthook/pre-commit/lint.rb).

Example

# lefthook.yml

pre-commit:
  scripts:
    "lint.js":
      runner: node
    "check.go":
      runner: go run

skip

You can skip all or specific commands and scripts using skip option. You can also skip when merging, rebasing, or being on a specific branch. Globs are available for branches.

Possible skip values:

  • rebase - when in rebase git state
  • merge - when in merge git state
  • merge-commit - when current HEAD commit is the merge commit
  • ref: main - when on a main branch
  • run: test ${SKIP_ME} -eq 1 - when test ${SKIP_ME} -eq 1 is successful (return code is 0)

Example

Always skipping a command:

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: true
      run: yarn lint

Skipping on merging and rebasing:

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip:
        - merge
        - rebase
      run: yarn lint

Or

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: merge
      run: yarn lint

Skipping when your are on a merge commit:

# lefthook.yml

pre-push:
  commands:
    lint:
      skip: merge-commit
      run: yarn lint

Skipping the whole hook on main branch:

# lefthook.yml

pre-commit:
  skip:
    - ref: main
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

Skipping hook for all dev/* branches:

# lefthook.yml

pre-commit:
  skip:
    - ref: dev/*
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

Skipping hook by running a command:

# lefthook.yml

pre-commit:
  skip:
    - run: test "${NO_HOOK}" -eq 1
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

TIP

Always skipping is useful when you have a lefthook-local.yml config and you don't want to run some commands locally. So you just overwrite the skip option for them to be true.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint
# lefthook-local.yml

pre-commit:
  commands:
    lint:
      skip: true

only

You can force a command, script, or the whole hook to execute only in certain conditions. This option acts like the opposite of skip. It accepts the same values but skips execution only if the condition is not satisfied.

Note: skip option takes precedence over only option, so if you have conflicting conditions the execution will be skipped.

Example

Execute a hook only for dev/* branches.

# lefthook.yml

pre-commit:
  only:
    - ref: dev/*
  commands:
    lint:
      run: yarn lint
    test:
      run: yarn test

When rebasing execute quick linter but skip usual linter and tests.

# lefthook.yml

pre-commit:
  commands:
    lint:
      skip: rebase
      run: yarn lint
    test:
      skip: rebase
      run: yarn test
    lint-on-rebase:
      only: rebase
      run: yarn lint-quickly

tags

You can specify tags for commands and scripts. This is useful for excluding. You can specify more than one tag using comma.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      tags:
        - frontend
        - js
      run: yarn lint
    test:
      tags:
        - backend
        - ruby
      run: bundle exec rspec

env

You can specify some ENV variables for the command or script.

Example

# lefthook.yml

pre-commit:
  commands:
    test:
      env:
        RAILS_ENV: test
      run: bundle exec rspec

Extending PATH

If your hook is run by GUI program, and you use some PATH tweaks in your ~/.rc, you might see an error saying executable not found. In that case You can extend the $PATH variable with lefthook-local.yml configuration the following way.

# lefthook.yml

pre-commit:
  commands:
    test:
      run: yarn test
# lefthook-local.yml

pre-commit:
  commands:
    test:
      env:
        PATH: $PATH:/home/me/path/to/yarn

Notes

This option is useful when using lefthook on different OSes or shells where ENV variables are set in different ways.

fail_text

You can specify a text to show when the command or script fails.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint
      fail_text: Add node executable to $PATH
$ git commit -m 'fix: Some bug'

Lefthook v1.1.3
RUNNING HOOK: pre-commit

  EXECUTE > lint

SUMMARY: (done in 0.01 seconds)
🥊  lint: Add node executable to $PATH env

stage_fixed

Default: false

Works only for pre-commit hook

When set to true lefthook will automatically call git add on files after running the command or script. For a command if files option was specified, the specified command will be used to retrieve files for git add. For scripts and commands without files option {staged_files} template will be used. All filters (glob, exclude) will be applied if specified.

Example

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: npm run lint --fix {staged_files}
      stage_fixed: true

interactive

Default: false

Note: If you want to pass stdin to your command or script but don't need to get the input from CLI, use use_stdin option instead.

Whether to use interactive mode. This applies the certain behavior:

  • All interactive commands/scripts are executed after non-interactive. Exception: piped option is set to true.
  • When executing, lefthook tries to open /dev/tty (Linux/Unix only) and use it as stdin.
  • When no_tty option is set, interactive is ignored.

use_stdin

Note: With many commands or scripts having use_stdin: true, only one will receive the data. The others will have nothing. If you need to pass the data from stdin to every command or script, please, submit a feature request.

Pass the stdin from the OS to the command/script.

Example

Use this option for the pre-push hook when you have a script that does while read .... Without this option lefthook will hang: lefthook uses pseudo TTY by default, and it doesn't close stdin when all data is read.

# .lefthook/pre-push/do-the-magic.sh

remote="$1"
url="$2"

while read local_ref local_oid remote_ref remote_oid; do
  # ...
done
# lefthook.yml
pre-push:
  scripts:
    "do-the-magic.sh":
      runner: bash
      use_stdin: true

priority

Default: 0

Note: This option makes sense only when parallel: false or piped: true is set.

Value 0 is considered an +Infinity, so commands or scripts with priority: 0 or without this setting will be run at the very end.

Set priority from 1 to +Infinity. This option can be used to configure the order of the sequential steps.

Example

# lefthook.yml

post-checkout:
  piped: true
  commands:
    db-create:
      priority: 1
      run: rails db:create
    db-migrate:
      priority: 2
      run: rails db:migrate
    db-seed:
      priority: 3
      run: rails db:seed

  scripts:
    "check-spelling.sh":
      runner: bash
      priority: 1
    "check-grammar.rb":
      runner: ruby
      priority: 2

Examples

Commitlint and commitzen

Use lefthook to generate commit messages using commitzen and validate them with commitlint.

Install dependencies

yarn add -D @commitlint/cli @commitlint/config-conventional
# If using commitzen
yarn add -D commitizen cz-conventional-changelog

Configure

Setup commitlint.config.js. Conventional configuration:

echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js

If you are using commitzen, make sure to add this in package.json:

"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  }
}

Test it

# You can type it without message, if you are using commitzen
git commit

# Or provide a commit message is using only commitlint
git commit -am 'fix: typo'

# lefthook.yml

# Use this to build commit messages
prepare-commit-msg:
  commands:
    commitzen:
      interactive: true
      run: yarn run cz --hook # Or npx cz --hook
      env:
        LEFTHOOK: 0

# Use this to validate commit messages
commit-msg:
  commands:
    "lint commit message":
      run: yarn run commitlint --edit {1}
# commitlint.config.js

module.exports = {extends: ['@commitlint/config-conventional']};

Remotes

Use configurations from other Git repositories via remotes feature.

Lefthook will automatically download the remote config files and merge them into existing configuration.

remotes:
  - git_url: https://github.com/evilmartians/lefthook
    configs:
      - examples/remote/ping.yml

Stage fixed files

Works only for pre-commit Git hook

Sometimes your linter fixes the changes and you usually want to commit them automatically. To enable auto-staging of the fixed files use stage_fixed option.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint {staged_files} --fix
      stage_fixed: true

Filters

Files passed to your hooks can be filtered with the following options

In this example all staged files will pass through these filters.

# lefthook.yml

pre-commit:
  commands:
    lint:
      run: yarn lint {staged_files} --fix
      glob: "*.{js,ts}"
      root: frontend
      exclude:
        - *.config.js
        - *.config.ts
      file_types:
        - not executable

Imagine you've staged the following files

backend/asset.js
frontend/src/index.ts
frontend/bin/cli.js # <- executable
frontend/eslint.config.js
frontend/README.md

After all filters applied the lint command will execute the following:

yarn lint frontend/src/index.ts --fix