Skip to content

Benchmarks ​

This benchmark compares hk, lefthook, pre-commit, and prek on everyday tasks: fixing files, checking a repository, and running pre-commit hooks on a large and a small commit. It measures elapsed time and verifies the files each tool produces. hk runs steps concurrently, using file locks to prevent fixers from writing the same file at once.

Measured 2026-10-13 on 8 CPUs (Intel(R) Core(TM) Ultra 5 325), Ubuntu 24.04.4 LTS. hk 2.6.0, lefthook 2.1.15, pre-commit 4.6.2, prek 0.5.4. Workflow run

Shorter bars mean faster runs. Bars show median times; lines span the fastest to slowest samples. Every tool produced the expected files in every timed sample.

Fix every file

A quarter of the files need two or three fixers each to write them.

hk and prek are level.

  • hkparallel, file locks10.4 s
  • prekfixers in turn, parallel checks, batched11.8 s
  • pre-commitsequential hooks, batched files13.4 s
  • lefthookfixers in turn, parallel checks17.9 s
Timing details
ToolMedianMean ± standard deviationRangeRuns
hk10.4 s11.7 s ± 2.12 s9.92 s–15.5 s10
prek11.8 s11.8 s ± 52 ms11.7 s–11.9 s10
pre-commit13.4 s13.4 s ± 54 ms13.4 s–13.5 s10
lefthook17.9 s17.9 s ± 121 ms17.7 s–18.1 s10

Check every file

Every file is already clean and nothing writes.

hk is 1.6× faster than lefthook, the fastest other tool.

  • hkparallel, file locks3.22 s
  • lefthookparallel: true5.04 s
  • prekshared priority, batched files5.83 s
  • pre-commitsequential hooks, batched files7.24 s
Timing details
ToolMedianMean ± standard deviationRangeRuns
hk3.22 s3.19 s ± 85 ms3.05 s–3.34 s10
lefthook5.04 s5.08 s ± 172 ms4.87 s–5.34 s10
prek5.83 s6.33 s ± 1.94 s4.53 s–9.71 s10
pre-commit7.24 s7.80 s ± 1.99 s5.86 s–11.3 s10

Commit

About 60 staged files with defects, fixed by each tool's pre-commit hook.

hk is 1.3× faster than lefthook, the fastest other tool.

  • hkparallel, file locks1.37 s
  • lefthookfixers in turn, parallel checks1.81 s
  • prekfixers in turn, parallel checks, batched2.16 s
  • pre-commitsequential hooks, batched files2.97 s
Timing details
ToolMedianMean ± standard deviationRangeRuns
hk1.37 s1.41 s ± 109 ms1.29 s–1.64 s20
lefthook1.81 s1.81 s ± 21 ms1.76 s–1.85 s20
prek2.16 s2.16 s ± 24 ms2.12 s–2.22 s20
pre-commit2.97 s2.97 s ± 38 ms2.92 s–3.09 s20

Small commit

One staged file with defects in each of eight file types.

hk is 1.2× faster than lefthook, the fastest other tool.

  • hkparallel, file locks1.04 s
  • lefthookfixers in turn, parallel checks1.27 s
  • prekfixers in turn, parallel checks, batched1.27 s
  • pre-commitsequential hooks, batched files2.02 s
Timing details
ToolMedianMean ± standard deviationRangeRuns
hk1.04 s1.04 s ± 71 ms950 ms–1.13 s20
lefthook1.27 s1.26 s ± 11 ms1.25 s–1.29 s20
prek1.27 s1.28 s ± 11 ms1.25 s–1.29 s20
pre-commit2.02 s2.02 s ± 22 ms2.00 s–2.07 s20

What the results show ​

Every tool produces the expected files in every timed sample. Each chart states how hk compares with the fastest other tool in that scenario. The scenarios differ in how much of each tool's run can overlap:

  • Fix every file: each hook has thousands of files, so pre-commit and prek split them into batches that keep every CPU busy. Running different fixers at the same time has little idle CPU to reclaim, and a quarter of the files need two or three fixers in turn.
  • Check every file: nothing writes, so lefthook and prek also run every check at once. The comparison is between tools that all run their checks concurrently, except pre-commit, which has no mode for it.
  • Commit and small commit: each hook has few files, so batching cannot fill the CPUs, and much of each hook's time is spent starting its tool. hk starts independent steps together and stages each step's files as it finishes. The other configurations run fixers one at a time.

Workload and correctness ​

The generated repository contains about 6,000 files: 4,000 Python, 500 JavaScript and TypeScript, 500 JSON, 500 shell, 250 YAML, 200 CSS, and 200 Markdown. Each configuration runs ten fixers: black, ruff format, ruff check, Prettier, ESLint, jq, yq, shfmt, trailing whitespace, and final newline. It also runs two type checkers, mypy and tsc, which change no files. mypy checks the Python files it is given. tsc checks the whole TypeScript project whenever a TypeScript file is selected, as it would in a real repository.

The generator creates two commits: clean, the result of running the fixers sequentially, and dirty, with defects in a quarter of the files: formatting for every file type, plus an unused import in each Python file that only ruff check removes. Each defective file needs a language fixer and a whitespace fixer, and a Python file needs black and ruff check as well, so the workload exercises overlapping writes.

ScenarioStarting stateMeasured work
Fix every filedirtyhk fix --all and equivalent commands.
Check every filecleanRead-only checks of the entire repository, as in CI.
CommitAbout 60 files with defects stagedEach tool's pre-commit hook fixes the staged files.
Small commitOne file with defects staged in each of eight file typesThe same hook on a commit of typical size.

With about 60 files, splitting each hook's files into batches keeps several CPUs busy. With one file per hook, there is nothing to split: most of the run is the time each tool takes to start, and a tool that runs its hooks one at a time waits for every start in turn.

The commit scenarios measure one hook invocation. hk and lefthook stage their fixes. pre-commit and prek leave fixes unstaged and return a failure, requiring the user to stage the changes and retry the commit. That manual work and retry are outside the measurement.

After every timed sample, tak checks that the resulting tree matches clean byte for byte. hk and lefthook must also exit successfully, since a failed type check changes no files. pre-commit and prek report failure whenever a hook modified files, so their exit codes cannot show a failed type check; they run the type checkers after every fixer has finished, and setup verifies that both commits type-check, including mypy on exactly the files each commit scenario stages. A separate check verifies that each tool detects defects in dirty. A run is publishable only if every tool passes all required checks.

Tool configurations ​

Each configuration uses the tool's fastest setting that cannot cause overlapping writes. hk runs its steps in parallel and uses file locks to keep them from colliding. In fix runs the type checkers also take write locks, because mypy and tsc declare that their commands write caches. pre-commit and prek give each batch different files. lefthook and prek also run read-only work concurrently.

ToolFixingChecking
hkSteps run concurrently with per-file locks.Same configuration.
lefthookFixers run sequentially (the default); then mypy and tsc run together in a parallel: true group.Jobs run concurrently with parallel: true.
pre-commitHooks run sequentially; each hook's file batches run across CPUs.Same configuration.
prekFixers run sequentially; then mypy and tsc run together with a shared priority. Each hook's file batches run across CPUs.Hooks run concurrently with a shared priority.

lefthook's parallel: true and prek hooks with a shared priority can run fixers that write the same file at once. Those modes are never used for fixers, even if a particular run happens to produce correct output. They are used only for hooks that write nothing to the files being checked. pre-commit has no mode for running hooks concurrently.

hk uses its builtins, including their check-before-fix behavior and the hk util whitespace fixers. The other configurations invoke the language fixers directly. pre-commit and lefthook use the whitespace fixers from pre-commit-hooks; prek uses its bundled Rust replacements. See the complete tool configurations.

Measurement method ​

  • Pinned versions. Hook managers, linters, and runtimes are pinned in benchmark/mise.toml. The charts list the measured hook-manager versions and host.
  • Interleaved samples. tak measures every tool once per round, in a new random order each round, to spread the effects of changing host conditions across tools.
  • Repeatable starting state. Each tool has its own clone, reset before each sample outside the timed interval. Each clone keeps its own warm caches, including ruff's and black's.
  • Isolated Git configuration. Global and system Git configuration is disabled to exclude the host's hooks, signing, and filesystem monitor.
  • Variation matters. The chart calls a tool faster only when the gap between medians exceeds both tools' sample ranges. Otherwise, it calls them level. This is a conservative comparison rule, not a statistical significance test.

Limitations ​

The generated files are uniform. Results in your repository depend on the linters, overlapping file patterns, number of changed files, and available CPU cores. When every tool already keeps all CPUs busy, as when fixing thousands of files split into batches, running steps concurrently has little idle time to reclaim. It gains most when individual hooks cannot use every CPU: small commits, whole-project checks, and single-threaded linters. Linters that already use all available cores, such as black, may gain less from running alongside other steps. Use hk timing reports to see where your own runs spend their time.

The benchmark excludes installation, first runs with cold caches, and hook-manager features beyond running fixers. It also does not measure the cost of preserving partially staged work: the commit scenario has no unstaged changes to save and restore.

Reproduce the benchmark ​

On Linux, install mise and run this command from a checkout of hk. The benchmark scripts require GNU sed; mise installs the pinned toolset.

sh
mise run benchmark

This builds hk from the checkout, generates the test repository in ~/.cache/hk-bench, runs the scenarios and correctness checks, and writes benchmark/results.json, which supplies the charts on this page.

To investigate one scenario, pass flags through to tak:

sh
mise run benchmark -- --bench fix-staged --runs 5

A filtered run is not publishable. To measure an existing hk binary instead of building the checkout, set HK_BIN to its path:

sh
HK_BIN=/path/to/hk mise run benchmark

See the benchmark maintainer guide for the scripts and publication workflow.

MIT LicenseCopyright © 2026jdx.dev