hk

repository·main·Indexed 21 days ago

https://github.com/jdx/hk

A 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.

Tokens
57K
Snippets
238
Records
321
Agent score
64%

What's inside hk

  1. What is hk?

    main
    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.
  2. Use per-directory environments in monorepos

    main

    When HK_MISE=1 is enabled, hk resolves the mise environment for each step's dir by running mise env in that directory (cached once per directory per run). This means tools and environment variables defined in a subdirectory's mise.toml are automatically available to steps running in that directory, even if hk was 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"
                }
            }
        }
    }
  3. Core concepts of hk's parallelism and file locking

    main
    Unlike other git hook managers that use naive parallel execution of shell scripts, hk uses 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 allows hk to run tasks in parallel safely, even when linters perform file edits.
  4. How hk handles unstaged changes with the stash setting

    main
    To 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), hk can automatically stash unstaged changes before running "fix" hooks. This is controlled via the stash setting.
  5. How hk achieves safe parallel linting

    main

    Unlike other tools (like lefthook in parallel mode, which can suffer from race conditions when linters touch overlapping files), hk runs linters in parallel safely by using file-level read/write locks.

    hk avoids write locks and race conditions through several mechanisms depending on the linter:

    LinterMechanismDescription
    prettiercheck_list_filesOnly locks files that actually need fixing.
    blackcheck_diffhk applies the diff itself.
    ruff checkRead lockPerforms check only.
    ruff formatcheck_diffhk applies the diff itself.
    jq, yq, shfmtcheck_diffhk applies the diff itself.
    trailing-whitespace, newlinescheck_diffUses built-in Rust hk util to apply diffs.
    eslintWrite lockFalls back to a standard write lock as it lacks a diff/list mode.

    This coordination allows hk to handle overlapping globs (e.g., a linter targeting *.js and another targeting **/*) without the corruption or race conditions seen in tools that lack file-level coordination.

  6. Organize steps using `<GROUP>`

    main

    A Group is 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 depends property.

    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 optionInherited step optionType
    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}}" }
                    }
                }
            }
        }
    }
  7. Understanding the hk plugin model and security

    main

    Unlike pre-commit or prek, which download and execute code from external third-party git repositories, hk uses a local, transparent plugin model based on Pkl configuration.

    hk builtins are hosted within the hk repository 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 }}"
    }
  8. Understand hk configuration precedence

    main

    hk builds its effective configuration by layering sources. Higher layers override lower layers. For hooks and steps, layers are additive: hkrc can define hooks that a project doesn't have, but a project's definition wins if there is a collision.

    PrecedenceSourceScope
    1 (lowest)Built-in defaultsAll projects
    2hkrc (~/.config/hk/config.pkl)All projects (user-level)
    3Project config (hk.pkl or hk.local.pkl)Single project
    4Git config (global, then local)Per-repo
    5Environment variables (HK_*)Per-invocation
    6 (highest)CLI flagsPer-invocation
  9. Configure global hk settings via hkrc

    main
    You can create a global configuration file at ~/.config/hk/config.pkl. This file is automatically merged into every project's hk.pkl, allowing you to enforce consistent linting rules or settings across all your repositories.
  10. How hk manages parallel execution and file locks

    main

    Unlike many hook managers that run tasks sequentially or risk race conditions during parallel execution, hk uses 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-different or --diff modes).
    • Write Locks: An exclusive lock is acquired when a linter needs to modify files (e.g., prettier --write).
    • Optimization: hk avoids unnecessary write locks by leveraging linter capabilities like ruff --diff or prettier --list-different. If a linter lacks these capabilities (like eslint), hk falls back to a write lock on all its targeted files, while other linters targeting different files continue to run in parallel.
    • check_first feature: For linters that don't support diff/list modes, you can use check_first to run a check first, and only run the fix command if the check fails.
  11. Configure mise integration

    main

    Setting HK_MISE=true enables deep integration with mise:

    • hk install: Uses mise x to execute hooks, removing the need to manually activate mise.
    • hk init: Generates a mise.toml file configured for hk.
    • Step execution: When running steps in a directory, hk resolves the mise environment for that directory (mise env), making tools and environment variables available.
  12. Configure hk.pkl with amends and imports

    main

    Every hk.pkl file should begin with an amends statement. This line performs schema validation and provides the base classes required for your configuration.

    Amends

    The amends statement points to a specific version of the Config.pkl file, 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