Skip to content

Why hk? ​

hk is designed for projects that run several linters and formatters over overlapping files. It combines concurrent execution with file-level coordination, so tools can work together without writing to the same file at the same time.

Parallelism needs coordination ​

A read-only check can run alongside other checks. A formatter needs exclusive access to the files it changes. Without that coordination, two formatters can read the same original content and overwrite one another’s fixes.

Within a hook run, hk tracks read/write locks for the files selected by each step:

WorkLockWhat can run alongside it?
Check a fileReadOther readers of that file
Fix a fileWriteSteps using different files
Check or fix unrelated filesIndependent locksOther steps, up to the job limit

This relies on accurate step definitions. A check must be read-only, and a step must declare the files its commands may touch. Commands that modify undeclared files bypass that protection. Use depends or exclusive when a tool’s effects extend beyond its selected files.

Use each linter’s capabilities ​

Fixers on different files run at the same time. A fix holds write locks on its step’s files, so only fixers that share files run one at a time. A step that only has a check command declaring effect = "read" changes no files, so even under hk fix it holds read locks and runs alongside other steps that read the same files; it still waits for a fixer that writes them. A check that does not declare a read effect, such as the builtin type checkers, whose caches may be written, holds write locks. hk’s builtins describe more efficient ways to run tools when they support them.

Diff output ​

A check_diff command emits a patch without editing files. When fixing, hk runs it and applies the patch itself instead of running fix. If the patch doesn’t apply, hk runs fix instead, or, for a step without fix, the command it runs when checking. When the command declares effect = "read", as most builtins do, hk computes the patch under read locks, so the formatter runs alongside other steps that read the same files. It takes write locks only on the files the patch changes, and only to apply it. If another step changed one of those files in the meantime, hk computes the patch again under write locks. A check_diff that doesn’t declare a read effect holds write locks throughout, as any fix does. Builtins such as oxfmt and Ruff’s formatter use this approach.

Lists of files needing fixes ​

A check_list_files command reports which files need changes. When the step checks first, as described below, Prettier’s --list-different lets hk pass only those files to --write.

Check before fixing ​

For other tools, a step can set check_first = true to run its check before its fix and skip the fix when the check passes. When files need fixing, the tool runs twice, and in hk's benchmark that cost more than it saved, so it's off by default.

These strategies affect orchestration overhead. Actual speed depends on your linters, file overlap, number of changed files, and available CPU cores. See the benchmarks for a reproducible workload and its limitations.

Work with partial commits ​

When a pre-commit hook uses stash = "git", hk temporarily saves unstaged changes, runs the steps against the staged versions, and restores the saved work afterward.

The unit of linting is a file, not a staged hunk. If you stage one function, a formatter can still reformat the whole staged version of that file. Stashing keeps unrelated work out of that version; it does not make the formatter operate on individual hunks.

Read hooks and stashing for automatic staging, review-before-commit settings, and recovery guidance.

Reuse configuration, keep control of tools ​

Builtins are Pkl step definitions: file patterns and commands you can inspect and amend. They invoke tools available in your environment. hk also includes native utilities for tasks such as trailing whitespace and merge conflict checks.

Pkl imports can refer to local files or remote packages. Pin package versions, review imported configuration, and manage linter versions as you would other project dependencies. A configuration that defines a shell command determines what hk will execute.

Is hk a fit? ​

hk is useful when you want:

  • The same steps in Git hooks, local checks, and CI.
  • Concurrent checks and coordinated fixes over shared files.
  • Reusable configuration with types, imports, and local overrides.
  • Control over tool installation through mise or an existing package manager.

The tradeoffs are a configuration language to learn and responsibility for providing your tools. File locks coordinate access; they cannot reconcile formatters with incompatible style rules. Choose compatible rules or use depends to establish the order your project needs.

Moving from another hook manager ​

You can evaluate hk on a branch before changing your team’s setup. Create a configuration, run hk check --all --plan, then compare the checks and fixes with your existing workflow.

For a pre-commit or prek configuration, start with hk migrate pre-commit. Known hooks become hk builtins and local shell hooks become hk steps. Everything else keeps running through prek or pre-commit, so you can switch now and convert the rest later.

Get started or browse the configuration examples.

MIT LicenseCopyright © 2026jdx.dev