github jdx/hk v2.5.0
v2.5.0: Safer stashing, faster config loading, and diagnostics for more builtins

2 hours ago

hk 2.5.0 makes stashing safe when hk is killed and when several hk processes run at once, loads configs much faster, and reports diagnostics from many more builtins. It also fixes a long list of cases where a check passed without checking anything, along with several Windows problems.

Highlights

  • Stashing holds up under interruption and concurrency. SIGTERM, SIGHUP and crashes no longer leave your unstaged changes stuck in git stash. Hooks running in linked worktrees now take turns stashing. Several edge cases that dropped or mixed up changes are fixed.
  • Configs load faster. A cold config load drops from about 1.4 s to about 0.07 s, and hk validate is about 14x faster. Several per-step and stashing costs are lower in large repos.
  • Fewer checks pass when they should fail. hk validate now catches more config mistakes. Several builtins and hk util checks no longer skip binary files, case conflicts in directory names, or executable bits recorded in git.

Added

  • Diagnostics from more builtins. actionlint, flake8, revive, buf_lint, clang_format, mypy, mado, rumdl, ktlint, buildifier_lint, cpp_lint, xmllint and sorbet now feed hk check --sarif, --format json and the MCP dashboard. Their normal output doesn't change. (#1623, #1624, #1626, #1627, #1630, @jdx)

  • Broader gcc diagnostic format and a new diagnostic_severity step setting (#1629, #1630, @jdx). The gcc parser now reads:

    • lines with no column (path:line: message)
    • a vet: prefix and Go # pkg header lines
    • tool output that ends with a summary line, which is no longer added to the last finding

    diagnostic_severity (error, warning, note or help; default error) sets the severity for rule-name: message findings that don't name one.

  • expect.diagnostics in step tests (#1622, @jdx). hk test can check that a step's diagnostic_format turns its output into the diagnostics you expect. Fields you leave out match anything, and message matches a substring.

    tests {
      ["reports a warning"] {
        expect {
          code = 1
          diagnostics {
            new { path = "src/main.c"; line = 2; severity = "warning"; rule = "W1" }
          }
        }
      }
    }
  • exec_ok() for step conditions (#1610, @jdx). exec_ok(command) is true when the command exits 0. Use it to gate a step on a shell test. exec() still returns stdout, and now gives a clear error instead of panicking on bad arguments or output that isn't valid UTF-8.

    condition = "exec_ok('test -f check.js')"
  • MCP scope argument (#1600, #1640, @jdx). start_check, start_safe_check and start_safe_fix accept scope:

    • all (default): the whole project, as before
    • changed: staged, unstaged and untracked files
    • unstaged: unstaged and untracked files
    • staged: only files in the index
  • hk agent stop-hook (#1604, @jdx). The Stop hooks that hk agent hooks --target claude-code|codex generates now run this command. It runs hk run check --safe, always exits 0, and prints a valid {"decision":"block","reason":...} when the check fails. Before, the hooks printed raw JSON and exited with the linter's status, which the agents didn't handle. Regenerate your hook snippets to use it.

  • hk validate warnings (#1590, @jdx). hk validate now warns about:

    • misspelled properties, with a did-you-mean suggestion
    • depends entries that have no effect
    • steps with no command
    • globs that can never match, such as ./src/*.js, a leading /, ! patterns or comma-separated lists

    min_hk_version now accepts v1.2.3 and two-part versions like 2.5.

  • hk init detects 47 more tools from their own config files or from manifest entries that name them. Examples include .flake8, stylua.toml, deno.json, .sqlfluff, .clang-format, [tool.isort] and typos. (#1576, @jdx)

  • Stash lock timeout. HK_STASH_LOCK_TIMEOUT (or git config hk.stashLockTimeout) sets how long hk waits for another hk process that is stashing. The default is 300 seconds, and 0 fails at once. (#1636, @jdx)

Changed

  • Cold config loads are about 20x faster. A config now evaluates only the builtins it uses, which cuts a cold load from about 1.4 s to about 0.07 s (#1581). The pklr 4.0 update makes hk validate on hk's own config about 14x faster (#1644). (@jdx)

  • Other speedups:

    • workspace_indicator is resolved once per directory, saving about 120 ms per step in large monorepos (#1599).
    • Runs that name their files, such as hk check FILE, skip the untracked-file scan (#1587).
    • Stashing no longer rescans the worktree first, saving about 80 ms on large repos (#1595).
    • Unchanged unstaged files are restored with one git process (#1612).
    • hk now uses the mimalloc memory allocator (#1635).

    (@jdx)

  • Read-only checks run in parallel in fix hooks (#1586, @jdx). In fix hooks, steps that only have a check declared with effect = "read" now run side by side instead of one after another. They still wait for any fixer that writes the same files. The builtin mypy and tsc declare effect = "write", so they are not affected.

  • CRLF files stay CRLF in trailing_whitespace, newlines and mixed_line_ending (#1621, @jdx).

    • trailing-whitespace strips only spaces and tabs, so it no longer strips form feeds or other Unicode whitespace.
    • end-of-file-fixer appends the file's most common line ending.
    • mixed-line-ending --fix converts to the file's most common line ending instead of always LF. On a tie it uses LF.
  • User config lists are merged with the project's (#1596, @jdx). skip_steps, skip_hooks and hide_warnings from the user config are now combined with the project's lists, as the docs describe. Before, the project's list replaced the user's.

    • hk now warns when hk.local.pkl doesn't amend hk.pkl, because the local file then replaces the shared config. Hide the warning with HK_HIDE_WARNINGS=local-config-replaces-shared.
  • Subproject skip_steps now apply to that subproject's own steps. hk now warns about other top-level subproject settings that have no effect. (#1615, @jdx)

  • Clearer error messages.

    • Failures print once, without Rust Location: lines. hk no longer suggests a fix command when the tool isn't installed. (#1588)
    • Argv steps say "command not found" when the tool is missing (#1579).
    • Pkl syntax errors show the file, line and column, including in imported files (#1598).
    • Hook '…' not found explains why: there is no hk.pkl, it's empty, or the name is wrong (it lists the defined hooks). hk init prints next steps. (#1564)

    (@jdx)

  • Interrupted steps report cancelled. In structured output and MCP, a step or run stopped by Ctrl-C now reports cancelled instead of failed (#1643, @jdx).

  • Capped structured output. Each step's output in JSON, JSONL, MCP and JUnit results is limited to 64 KiB. JSON output now includes a failed run_result when hk fails before any step runs, for example on a broken config. (#1605, @jdx)

Fixed

Stashing

  • If hk is stopped by SIGINT, SIGTERM or SIGHUP (or a Windows console close), it restores stashed changes before exiting. After a crash or kill -9, the next hk run restores them if the working tree is clean. Otherwise it prints the git stash apply command to run. (#1641, @jdx)
  • hk processes in the same repository, including linked worktrees, now take turns stashing and restoring (#1636). hk restores its own stash entry by commit id, so other stash entries are never touched (#1637). (@jdx)
  • Stashing now handles these cases (@jdx):
    • A merge or cherry-pick in progress is kept (#1609).
    • A staged edit is kept when its worktree copy was reverted to HEAD (#1602).
    • Tracked files whose names start with : are stashed (#1614).
    • Unstaged text over a staged binary file is restored (#1580).
    • CRLF line endings are kept when unstaged files are restored (#1535).

Steps and file selection

  • Steps that depend on a step with no files now run instead of hanging (#1584).
  • Diagnostics from every job that started are kept when another job fails (#1642).
  • An exclude that names a directory now excludes the files in it (#1606).
  • Files are selected in directories whose names contain brackets or braces (#1592).
  • Tracked files whose names aren't valid UTF-8 are skipped with a warning in --all, --glob, --from-ref and file arguments (#1613).
  • Diagnostic paths are relative to the repository root for steps that run in a subdirectory (#1638).
  • Scripts whose env shebang uses -S, options or variables now get the right file type (#1611).
  • interactive = true steps run in hk's process group, so TUI tools such as fzf, helix and gimoji display correctly (#1557).
  • Formatter patches are applied and rolled back without interference from other commands or staging (#1392, @nettlesh).

Configuration and CLI

  • Top-level jobs in hk.pkl or the user config is now applied (#1594).
  • HK_STASH accepts true/1 and false/0, and an invalid value gives an error instead of a panic (#1594).
  • hk config no longer crashes when a setting is unset (#1594).
  • Dependency cycles, steps that depend on themselves, and invalid globs or regexes are rejected when the config loads, with the hook and step named. Before, hk check could hang. (#1567)
  • hk check --step with an unknown name now fails instead of passing after running nothing (#1603).
  • Configs that amend the v1 package schema load again (#1560).
  • Mirror credentials are hidden in errors from failed package downloads (#1620).
  • --help, --version and shell completion keep their normal exit code when the reader closes the pipe early (#1540).

Installed hooks

  • hk install --global records a path that still works after upgrades: the mise shim or the hk on PATH, not a versioned install path. Local hooks add hk's directory to the end of PATH, so they work in GUI git clients with a minimal PATH. (#1597)
  • The message for hk install --force-local when a global install exists is now correct (#1616).

Builtins and hk util

  • check_added_large_files and check_case_conflict now check binary files (#1571).
  • no-commit-to-branch works when a tag has the same name as the branch (#1577).
  • The executable and shebang checks use the file mode recorded in git, so they also work on Windows and with core.fileMode=false (#1577).
  • check-case-conflict finds conflicts between directory names, such as Foo/ and foo/ (#1577).
  • Text checks no longer skip files when the 8 KiB sample ends in the middle of a multibyte character. Key checks now also handle files that aren't valid UTF-8. (#1572)
  • fix-smart-quotes leaves files that aren't valid UTF-8, and symlinks, unchanged (#1570).
  • check-conventional-commit accepts merge and revert titles generated by git (#1566).

MCP and agents

  • cancel_run stops the hk run instead of staying in cancelling (#1607).
  • get_diff includes untracked and binary files and reports capture errors (#1601).

Windows

  • Percent signs in file names stay literal under cmd.exe (#1568).
  • Batches for cmd shims stay under the cmd.exe command-line length limit (#1618).
  • A custom shell runs directly, and quoted shell paths work (#1628).
  • Tools still running when a fail-fast cancel stops a run are now ended (#1625).

Security

  • hk check --safe and hk fix --safe now refuse a hook-level report whose effect is unknown or destructive. report accepts the same command forms as check, so you can declare its effect. (#1573)
  • Patches from check_diff that write inside .git, in any spelling git treats as .git, are refused, and hk runs the fixer instead. On Unix, hk creates new state directories with mode 0700. (#1585)
  • Legacy hk install (Git older than 2.54, or --legacy) no longer overwrites hooks that hk didn't write and no longer writes through symlinks; see Breaking Changes (#1593).

Breaking Changes

  • Legacy installs refuse to replace foreign hooks. If .git/hooks/ contains a hook that hk didn't write, or a symlink, hk install changes nothing and lists the files. Pass --force to replace them; a symlink's target is left untouched. Setups where another tool owns hooks, such as git lfs install, need hk install --force. Config-based installs on Git 2.54+ are not affected. (#1593)
  • Line-ending fixers keep CRLF. Repos that relied on trailing_whitespace or mixed_line_ending converting CRLF files to LF need another way to normalize line endings. (#1621)
  • Signal exit codes. When stopped by a signal, hk exits with 128 + signal: 130 for Ctrl-C (previously 1), 143 for SIGTERM and 129 for SIGHUP. Pressing Ctrl-C twice exits at once. (#1641)
  • Stricter config checks.
    • Dependency cycles and invalid step globs now fail when the config loads (#1567).
    • --step with an unknown name fails (#1603).
    • min_hk_version values such as "2.6" or "v2.6.0" are now enforced. Before, they were ignored. (#1590)
  • --safe and plain-string reports. A plain-string hook report blocks --safe runs. Declare it as a CommandSpec with effect = "read" to keep using it under --safe. (#1573)

Full Changelog: v2.4.0...v2.5.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.