Migrating from lefthook
hk has no converter for lefthook. (hk migrate pre-commit handles only the pre-commit framework.) You translate lefthook.yml into hk.pkl by hand. The two tools share the same shape, hooks that contain named jobs that select files and run commands, so most configs map one to one.
Differences to check first
- Parallelism. lefthook runs a hook's jobs in sequence unless you set
parallel: true. hk runs steps concurrently by default, up to the job limit. It coordinates steps that select the same files with read and write locks, and you declare an order withdepends. A lefthook config that relies on its sequence needsdependsin hk. See order steps deliberately. - Check and fix. A lefthook
runis one command. hk separatescheck, which reports, fromfix, which edits. A formatter that rewrites files inpre-commitbecomes afixcommand in a hook withfix = true. - Staging fixes. lefthook stages changed files for jobs with
stage_fixed: true. hk'spre-commithook stages the files a step fixed by default, and a step'sstageglobs narrow or widen that. Other hooks leave fixes unstaged unlessstage = true. - Partial commits.
stash = "git"on a hook makes hk set aside unstaged changes while it runs and restore them afterward, so linters see the staged content. See stashing and partial commits. - Failures. hk stops after the first failing step by default (
fail_fast).hk run pre-commit --no-fail-fastreports every failure. - Globs. Both tools match a pattern without a slash against files at any depth, so
*.jsmatchesa.jsandsrc/b.jsin each. They differ for**/*.js: lefthook matches only files below a directory, and hk also matchesa.jsat the repository root.
Move over
- Write
hk.pklfrom yourlefthook.yml, using the mapping below.hk initcan generate a starting file for the tools it detects. See getting started. - Run
hk validate, then preview what a hook selects withhk run pre-commit --plan. - Remove lefthook's Git hooks with
lefthook uninstall. That deletes the hooks it installed in.git/hooks. It leaveslefthook.ymlunless you pass--remove-configs, so you can keep it for reference. - Run
hk install. On Git 2.54 and newer hk registers its hooks in Git config, which runs alongside any other hook manager, so remove lefthook's hooks first or both run. - Delete
lefthook.yml,lefthook-local.yml, and the lefthook dependency or tool pin.
Hooks, jobs, and options
| lefthook | hk |
|---|---|
pre-commit, commit-msg, pre-push | A hook of the same name under hooks. See other Git events for the others hk handles. |
commands.<name> or a jobs entry | steps { ["<name>"] { ... } } |
run | check for a command that reports, fix for one that edits files |
glob | glob, a string or List. A Regex also works. |
exclude | exclude |
root | dir |
env | env on the step, or on the hook for every step in it |
parallel: true | The default |
piped: true, priority | depends between steps |
stage_fixed: true | The pre-commit default, or stage on the hook or step |
skip, only | step_condition or condition, or profiles. See conditions. |
tags | profiles. A step with profiles runs only when you enable one of them with --profile or HK_PROFILE. |
interactive: true | interactive = true |
scripts | A step whose check or fix runs the script, for example check = "./scripts/lint.sh {{files}}" |
lefthook-local.yml | hk.local.pkl, which amends hk.pkl. See hk.local.pkl. |
extends, remotes | Pkl amends and import. See Pkl essentials. |
fail_text | No equivalent |
Placeholders
hk renders commands as Tera templates, so a placeholder is written with double braces.
| lefthook | hk |
|---|---|
{staged_files} in pre-commit | {{files}}, the files the hook selected that match the step. In pre-commit they are staged. |
{push_files} in pre-push | {{files}}. In pre-push hk selects the files changed by the push. |
{1} in commit-msg | {{commit_msg_file}} |
{1} {2} in pre-push | {{hook_args}}, the remote name and URL |
{all_files} | No placeholder. Use a command that finds its own files, or run hk run pre-commit --all. |
{{files}} is quoted for the shell and relative to the step's dir. See define a step and commit-message hooks.
Conditions
lefthook's skip: [merge] has no built-in in hk. Write the condition yourself. step_condition is an expression, and exec_ok(...) is true when a shell command exits with status 0:
["lint"] {
// Skip while a merge is in progress
step_condition = "!exec_ok('git rev-parse -q --verify MERGE_HEAD')"
check = "make lint"
}
["release-notes"] {
// Run only on main
step_condition = "exec_ok('test \"$(git branch --show-current)\" = main')"
check = "make release-notes"
}Use exec_ok to test whether a command succeeds. exec(...) returns a command's output instead, and a command that exits non-zero makes exec fail the hook. See conditions and Git status.
Commands and skipping
| lefthook | hk |
|---|---|
lefthook install | hk install |
lefthook uninstall | hk uninstall |
lefthook run pre-commit | hk run pre-commit |
lefthook run pre-commit --all-files | hk run pre-commit --all |
lefthook run pre-commit --file a --file b | hk run pre-commit a b |
lefthook run pre-commit --command eslint | hk run pre-commit --step eslint |
lefthook run pre-commit --fail-on-changes | fail_on_fix = true on the hook, with stage = false. See review fixes before committing. |
LEFTHOOK=0 git commit | HK=0 git commit |
LEFTHOOK_EXCLUDE=eslint git commit | HK_SKIP_STEPS=eslint git commit |
See skip a hook or step for the rest.
Example
A lefthook config that formats and lints staged files, checks the commit message, and runs tests before a push:
pre-commit:
parallel: true
commands:
prettier:
glob: "*.{js,ts,css,md}"
exclude: "dist/**"
run: npx prettier --write {staged_files}
stage_fixed: true
eslint:
glob: "*.{js,ts}"
run: npx eslint --fix {staged_files}
stage_fixed: true
commit-msg:
commands:
commitlint:
run: npx commitlint --edit {1}
pre-push:
commands:
test:
run: npm testThe same hooks in hk:
amends "package://github.com/jdx/hk/releases/download/v2.5.0/hk@2.5.0#/Config.pkl"
hooks {
["pre-commit"] {
fix = true
stash = "git"
steps {
["eslint"] {
glob = "*.{js,ts}"
fix = "npx eslint --fix {{files}}"
}
["prettier"] {
glob = "*.{js,ts,css,md}"
exclude = "dist/**"
depends = "eslint"
fix = "npx prettier --write {{files}}"
}
}
}
["commit-msg"] {
steps {
["commitlint"] { check = "npx commitlint --edit {{commit_msg_file}}" }
}
}
["pre-push"] {
steps {
["test"] { check = "npm test" }
}
}
}- The lefthook config ran
prettierandeslintin parallel on the same files.depends = "eslint"makes hk runeslintfirst, so the two never rewrite a file at the same time. Drop it if the order does not matter; hk's file locks still prevent simultaneous writes. pre-commitstages whateslintandprettierchanged. Nostage_fixedis needed.hk run pre-commit --planshows which steps and files a commit would use.
