hk
repository·main·Indexed 21 days ago
https://github.com/jdx/hkA high-performance git hook manager and project linting tool (v1.54.0) that uses read/write file locks to enable aggressive parallelism and prevent race conditions. It features over 140 pre-configured built-in linters and formatters, a mechanism to automatically stash unstaged changes before running fix hooks, and flexible execution controls via the hk check command.
What's inside hk
- hk is a git hook manager and project linting tool designed for high performance. It features tight integration with linters and utilizes read/write file locks to maximize concurrency while preventing race conditions during execution.
Use per-directory environments in monorepos
mainWhen
HK_MISE=1is enabled,hkresolves themiseenvironment for each step'sdirby runningmise envin that directory (cached once per directory per run). This means tools and environment variables defined in a subdirectory'smise.tomlare automatically available to steps running in that directory, even ifhkwas started from the repository root.hooks { ["check"] { steps { ["oxlint"] = (Builtins.ox_lint) { // with HK_MISE=1, tools from subproject/mise.toml are on PATH dir = "subproject" } } } }Core concepts of hk's parallelism and file locking
mainUnlike other git hook managers that use naive parallel execution of shell scripts,hkuses read/write file locks to enable aggressive parallelism. This prevents race conditions when multiple linters attempt to modify the same file simultaneously. This locking mechanism allowshkto run tasks in parallel safely, even when linters perform file edits.How hk handles unstaged changes with the stash setting
mainTo prevent the common issue where pre-commit hooks erroneously stage unstaged changes (often caused by linters modifying files that contain both staged and unstaged changes),hkcan automatically stash unstaged changes before running "fix" hooks. This is controlled via thestashsetting.How hk achieves safe parallel linting
mainUnlike other tools (like
lefthookin parallel mode, which can suffer from race conditions when linters touch overlapping files),hkruns linters in parallel safely by using file-level read/write locks.hkavoids write locks and race conditions through several mechanisms depending on the linter:Linter Mechanism Description prettiercheck_list_filesOnly locks files that actually need fixing. blackcheck_diffhkapplies the diff itself.ruff checkRead lock Performs check only. ruff formatcheck_diffhkapplies the diff itself.jq,yq,shfmtcheck_diffhkapplies the diff itself.trailing-whitespace,newlinescheck_diffUses built-in Rust hk utilto apply diffs.eslintWrite lock Falls back to a standard write lock as it lacks a diff/list mode. This coordination allows
hkto handle overlapping globs (e.g., a linter targeting*.jsand another targeting**/*) without the corruption or race conditions seen in tools that lack file-level coordination.Organize steps using `<GROUP>`
mainA
Groupis a collection of steps executed in parallel. It acts as a synchronization point: all steps in the group must finish before subsequent steps or groups can start.Note: Groups are a simple way to manage order, but for complex dependencies, use read/write locks or the
dependsproperty.Group Inheritance
Steps inside a group inherit certain settings from the group if they do not define them themselves. Values are overridden, not merged.
Group option Inherited step option Type dirdirString?prefixprefixString?workspace_indicatorworkspace_indicatorString?shellshell(String | Script)?stagestage(String | List<String>)?excludeexclude(String | List<String> | Regex)?hooks { ["pre-commit"] { steps { ["build"] = new Group { steps = new Mapping<String, Step> { ["ts"] = new Step { fix = "tsc -b" } ["rs"] = new Step { fix = "cargo build" } } } ["lint"] = new Group { steps = new Mapping<String, Step> { ["prettier"] = new Step { check = "prettier --check {{files}}" } ["eslint"] = new Step { check = "eslint {{files}}" } } } } } }Understanding the hk plugin model and security
mainUnlike
pre-commitorprek, which download and execute code from external third-party git repositories,hkuses a local, transparent plugin model based on Pkl configuration.hkbuiltins are hosted within thehkrepository as Pkl files. When you use a builtin, you are importing a configuration that defines how to invoke linters already installed on your system. This allows you to audit exactly what a hook does by reading its Pkl definition.Example of a Pkl builtin definition:
// This is an entire hk builtin. prettier = new Config.Step { glob = List("**/*.js", "**/*.ts", "**/*.css", "**/*.json", "**/*.md") check = "prettier --check {{ files }}" check_list_files = "prettier --list-different {{ files }}" fix = "prettier --write {{ files }}" }// This is an entire hk builtin. That's it. prettier = new Config.Step { glob = List("**/*.js", "**/*.ts", "**/*.css", "**/*.json", "**/*.md") check = "prettier --check {{ files }}" check_list_files = "prettier --list-different {{ files }}" fix = "prettier --write {{ files }}" }Understand hk configuration precedence
mainhk builds its effective configuration by layering sources. Higher layers override lower layers. For hooks and steps, layers are additive:
hkrccan define hooks that a project doesn't have, but a project's definition wins if there is a collision.Precedence Source Scope 1 (lowest) Built-in defaults All projects 2 hkrc(~/.config/hk/config.pkl)All projects (user-level) 3 Project config ( hk.pklorhk.local.pkl)Single project 4 Git config (global, then local) Per-repo 5 Environment variables ( HK_*)Per-invocation 6 (highest) CLI flags Per-invocation Configure global hk settings via hkrc
mainYou can create a global configuration file at~/.config/hk/config.pkl. This file is automatically merged into every project'shk.pkl, allowing you to enforce consistent linting rules or settings across all your repositories.How hk manages parallel execution and file locks
mainUnlike many hook managers that run tasks sequentially or risk race conditions during parallel execution,
hkuses a file-level read/write locking mechanism. This allows multiple linters to run in parallel safely.- Read Locks: Used when a linter only checks files without modifying them (e.g., using
--list-differentor--diffmodes). - Write Locks: An exclusive lock is acquired when a linter needs to modify files (e.g.,
prettier --write). - Optimization:
hkavoids unnecessary write locks by leveraging linter capabilities likeruff --difforprettier --list-different. If a linter lacks these capabilities (likeeslint),hkfalls back to a write lock on all its targeted files, while other linters targeting different files continue to run in parallel. check_firstfeature: For linters that don't support diff/list modes, you can usecheck_firstto run a check first, and only run the fix command if the check fails.
- Read Locks: Used when a linter only checks files without modifying them (e.g., using
Configure mise integration
mainSetting
HK_MISE=trueenables deep integration withmise:hk install: Usesmise xto execute hooks, removing the need to manually activate mise.hk init: Generates amise.tomlfile configured for hk.- Step execution: When running steps in a directory, hk resolves the
miseenvironment for that directory (mise env), making tools and environment variables available.
Configure hk.pkl with amends and imports
mainEvery
hk.pklfile should begin with anamendsstatement. This line performs schema validation and provides the base classes required for your configuration.Amends
The
amendsstatement points to a specific version of theConfig.pklfile, typically hosted on GitHub:amends "package://github.com/jdx/hk/releases/download/v1.54.0/hk@1.54.0#/Config.pkl"Imports
You can share code between files using
import. Supported sources include local file paths and HTTP URLs:import "./extra.pkl" import "https://example.com/remote.pkl"amends "package://github.com/jdx/hk/releases/download/v1.54.0/hk@1.54.0#/Config.pkl" import "./extra.pkl" // Use imported content here