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.
Fix every file
A quarter of the files need two or three fixers each to write them.
hk and prek are level.
Timing details
| Tool | Median | Mean ± standard deviation | Range | Runs |
|---|---|---|---|---|
| hk | 10.4 s | 11.7 s ± 2.12 s | 9.92 s–15.5 s | 10 |
| prek | 11.8 s | 11.8 s ± 52 ms | 11.7 s–11.9 s | 10 |
| pre-commit | 13.4 s | 13.4 s ± 54 ms | 13.4 s–13.5 s | 10 |
| lefthook | 17.9 s | 17.9 s ± 121 ms | 17.7 s–18.1 s | 10 |
Check every file
Every file is already clean and nothing writes.
hk is 1.6× faster than lefthook, the fastest other tool.
Timing details
| Tool | Median | Mean ± standard deviation | Range | Runs |
|---|---|---|---|---|
| hk | 3.22 s | 3.19 s ± 85 ms | 3.05 s–3.34 s | 10 |
| lefthook | 5.04 s | 5.08 s ± 172 ms | 4.87 s–5.34 s | 10 |
| prek | 5.83 s | 6.33 s ± 1.94 s | 4.53 s–9.71 s | 10 |
| pre-commit | 7.24 s | 7.80 s ± 1.99 s | 5.86 s–11.3 s | 10 |
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.
Timing details
| Tool | Median | Mean ± standard deviation | Range | Runs |
|---|---|---|---|---|
| hk | 1.37 s | 1.41 s ± 109 ms | 1.29 s–1.64 s | 20 |
| lefthook | 1.81 s | 1.81 s ± 21 ms | 1.76 s–1.85 s | 20 |
| prek | 2.16 s | 2.16 s ± 24 ms | 2.12 s–2.22 s | 20 |
| pre-commit | 2.97 s | 2.97 s ± 38 ms | 2.92 s–3.09 s | 20 |
Small commit
One staged file with defects in each of eight file types.
hk is 1.2× faster than lefthook, the fastest other tool.
Timing details
| Tool | Median | Mean ± standard deviation | Range | Runs |
|---|---|---|---|---|
| hk | 1.04 s | 1.04 s ± 71 ms | 950 ms–1.13 s | 20 |
| lefthook | 1.27 s | 1.26 s ± 11 ms | 1.25 s–1.29 s | 20 |
| prek | 1.27 s | 1.28 s ± 11 ms | 1.25 s–1.29 s | 20 |
| pre-commit | 2.02 s | 2.02 s ± 22 ms | 2.00 s–2.07 s | 20 |
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.
| Scenario | Starting state | Measured work |
|---|---|---|
| Fix every file | dirty | hk fix --all and equivalent commands. |
| Check every file | clean | Read-only checks of the entire repository, as in CI. |
| Commit | About 60 files with defects staged | Each tool's pre-commit hook fixes the staged files. |
| Small commit | One file with defects staged in each of eight file types | The 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.
| Tool | Fixing | Checking |
|---|---|---|
| hk | Steps run concurrently with per-file locks. | Same configuration. |
| lefthook | Fixers run sequentially (the default); then mypy and tsc run together in a parallel: true group. | Jobs run concurrently with parallel: true. |
| pre-commit | Hooks run sequentially; each hook's file batches run across CPUs. | Same configuration. |
| prek | Fixers 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.
mise run benchmarkThis 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:
mise run benchmark -- --bench fix-staged --runs 5A filtered run is not publishable. To measure an existing hk binary instead of building the checkout, set HK_BIN to its path:
HK_BIN=/path/to/hk mise run benchmarkSee the benchmark maintainer guide for the scripts and publication workflow.
