github jdx/hk v2.2.0
v2.2.0: Faster fixing by default, JUnit XML reports, and a reworked `hk migrate pre-commit`

2 hours ago

Fix steps now run their fixer directly instead of checking first, and batch steps start fewer processes on small commits. Together these make hk run pre-commit about a third faster on a typical fixture. This release also adds JUnit XML output, hook arguments in conditions, and a hk migrate pre-commit that produces a working config right away. It fixes a staging race between concurrent fixers and a stale config cache when Pkl reads environment variables.

Added

  • --junit-xml <PATH> for hk check, hk fix and hk run <hook> (#1432, @sanga). Writes one JUnit <testcase> per step, with its status, duration and captured output, so CI systems such as Jenkins can show hk results. It works alongside --sarif, whatever output format you choose. If a run fails before any step is planned, the report still contains a failing test case, so it doesn't look like a passing build.

  • Git hook arguments in condition and step_condition (#1445, @jdx). Conditions can now use the hook variables that command templates already had: prev_head, new_head and is_branch_checkout (post-checkout), commit_msg_file (commit-msg, prepare-commit-msg), source and sha (prepare-commit-msg), hook_stdin (pre-push, post-rewrite), and hook_args (all hooks). Conditions are expr-lang expressions, so write is_branch_checkout, not {{ is_branch_checkout }}.

    hooks {
      ["post-checkout"] {
        steps {
          ["install-deps"] {
            step_condition = "is_branch_checkout"
            check = "mise install"
          }
        }
      }
    }
  • hk migrate pre-commit produces a working config (#1459, @jdx).

    • Hooks from 34 well-known repos (66 hook ids) become hk builtins, unless they set args, files, types or other options the builtin can't reproduce.
    • Local system, script and fail hooks become native steps with their command, filters and stages kept.
    • Every other hook becomes a precommit("id", "stage") step that runs through prek or pre-commit, with a comment explaining why it wasn't converted. Pick the tool with the new --runner prek|pre-commit flag. The default is prek, unless only pre-commit is on PATH.
    • Hooks go into the matching git stage. manual hooks run only with hk check and hk fix.
    • Pre-commit's top-level exclude and fail_fast settings are honored.
    • The legacy commit stage now maps to pre-commit instead of commit-msg.

    Keep .pre-commit-config.yaml until every delegated step is converted, because those steps still read it. Delegated steps use the same command for check and fix, so hk check can modify files for them, just as pre-commit run does. The migration no longer clones hook repos into .hk/vendors/.

  • New builtins Builtins.renovate_deps and Builtins.lychee_extended (#1448, #1449, @zeitlinger). Both run Flint's checkers on the files hk selects. renovate_deps checks the Renovate dependency snapshot (.github/renovate-tracked-deps.json) and regenerates it in fix mode. It requires flint and renovate. lychee_extended checks links in text files and remaps links to the same repository in PRs. It requires flint and lychee.

  • svelte, vue and astro file types (#1428, @nettlesh). .svelte, .vue and .astro files are now tagged text plus their own type, so steps can select them with types = List("svelte", "vue") instead of globs. These tags don't imply html, javascript or typescript.

  • hk init finds source files in subdirectories (#1403, @zeitlinger). Detection respects .gitignore (or .ignore outside git), includes hidden files and doesn't follow symlinks. For example, nested shell scripts and BUILD.bazel files now enable the matching builtins. Manifest and config indicators such as Cargo.toml still count only at the repository root.

  • hk init detects native Biome and ESLint config files (#1402, @zeitlinger), including biome.jsonc, flat eslint.config.* and legacy .eslintrc*. Projects no longer need matching package metadata to get these steps.

  • cargo-fmt builtin watches rustfmt.toml and .rustfmt.toml (#1446, @sanga), so changing rustfmt settings reruns the step.

Changed

  • check_first now defaults to false (#1461, @jdx). Before, when several fixers wrote the same files, each ran its check and then ran fix only if the check failed. Files that needed fixing were processed twice (eslint, then eslint --fix). Steps now run fix directly. Set check_first = true on a slow fixer whose files usually pass already. hk still checks first on its own in these cases:

    • steps whose check and fix are the same command (pre-commit-style fixers). A single fixer like this now passes after fixing, even when no other step touches its files.
    • check_diff steps in fix mode.
    • check_diff or check_list_files steps in hooks that stage fixes. These fix and stage only the files that need it.

    On a benchmark with about 6,000 files and ten fixers, together with the batching change below, hk run pre-commit dropped from 1,442 ms to 965 ms and hk fix --all from 5,959 ms to 4,568 ms.

  • Batch steps run at least 4 files per process (#1458, @jdx). hk used to split files into files / jobs chunks, which on small commits meant one process per file. It now makes min(jobs, files / 4) batches and splits files evenly across them, like pre-commit and prek. For example, 5 files on 8 jobs run as a single process. Large file sets are split the same way as before.

  • Faster black, jq and yq builtins (#1454, @jdx). black no longer passes --diff, which bypassed black's cache. On about 4,000 formatted files the step went from roughly 7–9 s to 0.8 s. Failures now show black's would reformat <path> lines instead of a diff. jq and yq now set batch = true, so their files are split across jobs.

  • Top-level exclude from user config and project config now combine (#1460, @jdx). If ~/.config/hk/config.pkl and hk.pkl both set exclude, the project value used to replace the user value, which contradicted the docs. Both now apply.

Fixed

  • Staging no longer fails or stages half-written files when fixers overlap (#1453, @jdx). When several fixers rewrote the same files in parallel, hk run pre-commit could fail with errors like failed to add files to index … file changed before we could read it, index.lock: File exists, or could not find '…' to open, and could stage partial content. Staging now waits until every step writing those files is done.
  • Config cache follows environment variables read by Pkl (#1450, @christopher-buss). A config that branched on read?("env:..."), such as setting profiles when CLAUDECODE is set, kept whatever result the first run cached. The cache is now keyed on the value of every environment variable the evaluation read, including unset ones. The first run after upgrading misses the cache.
  • A Regex for top-level exclude now loads (#1460, @jdx). exclude = Regex(#"^vendor/|\.gen\."#) used to fail with data did not match any variant of untagged enum StringOrList. It now works like a step-level regex exclude. An invalid pattern fails hk validate. Top-level excludes also now apply to file arguments written as absolute paths or with .. segments.
  • yq fix no longer merges several files into the first one (#1452, @jdx). yq -iP a.yml b.yml writes both documents into a.yml and leaves b.yml unchanged. The builtin now fixes each file separately.
  • check_list_files works with a step's dir (#1454, @jdx). Paths printed relative to dir used to match nothing, and the fix aborted with check_list_files failed with no files in output. This affects prettier, gofmt, black and any other step using check_list_files.
  • Templates and XML files with unknown extensions count as text (#1427, @nettlesh). Files starting with an HTML-like tag (such as script.tmpl) or an XML declaration (such as App.csproj) were tagged binary, so types = List("text") steps skipped them. They are now tagged text, and XML files also get xml.
  • Closed output pipes no longer crash hk (#1442, @jdx). Commands like hk --version | true, a pager you quit early, or a cancelled shell completion used to panic with SIGABRT and leave a core dump. hk now exits like a program killed by SIGPIPE (status 141), with no panic output.
  • hk init --mise keeps an existing mise.toml (#1404, @zeitlinger). It now adds only missing hk and pre-commit entries and keeps existing pins, comments, tools and task definitions. It doesn't add a pre-commit task when an included task file already defines one. --force now affects only hk.pkl.

New Contributors

Full Changelog: v2.1.0...v2.2.0

💚 Sponsor hk

hk is built and maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.

If hk speeds up your pre-commit loop or makes linting less painful, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep hk fast, free, and independent.

Don't miss a new hk release

NewReleases is sending notifications on new releases.