Pkl essentials
hk uses Pkl for typed configuration. Most projects need only a few features: amend the schema, import builtins, define steps, and reuse them across hooks.
Pkl evaluates configuration. hk then runs the commands that configuration defines.
Start with the schema
Every project configuration should amend hk’s base schema:
amends "package://github.com/jdx/hk/releases/download/v2.0.1/hk@2.0.1#/Config.pkl"
import "package://github.com/jdx/hk/releases/download/v2.0.1/hk@2.0.1#/Builtins.pkl"amends supplies the allowed properties and classes, such as Step, Hook, and Group. import makes another module available under its name, here Builtins.
Keep both package URLs on the same version. Changing the hk executable does not rewrite pinned imports in hk.pkl.
Values and local variables
local label = "lint"
local workers = 4
local enabled = true
local extensions = List("*.js", "*.ts")Use local for helper values that are not part of hk’s schema. Without it, Pkl treats the value as a configuration property.
Strings use double quotes, booleans use true and false, and a list uses List(...).
Define a step
local eslint = new Step {
glob = List("*.js", "*.ts")
check = "eslint {{files}}"
fix = "eslint --fix {{files}}"
}new Step creates an instance of the schema’s step class. {{files}} is an hk command template, expanded later when the step runs; it is not Pkl interpolation.
Reuse steps in mappings
Hooks and steps are mappings keyed by name. Prefer top-level steps for linters shared by check, fix, and pre-commit:
steps {
["eslint"] = Builtins.eslint
["prettier"] = Builtins.prettier
}A mapping entry uses ["name"] = value. Each name must be unique within the mapping.
Top-level steps is optional. To share a mapping between selected explicit hooks instead, use a local helper:
local linters = new Mapping<String, Step> {
["eslint"] = Builtins.eslint
["prettier"] = Builtins.prettier
}
hooks {
["check"] { steps = linters }
["fix"] {
fix = true
steps = new Mapping<String, Step> {
...linters
["shellcheck"] = Builtins.shellcheck
}
}
}Amend a builtin
Parentheses followed by an object body create a modified copy:
steps {
["prettier"] = (Builtins.prettier) {
glob = List("*.js", "*.ts")
exclude = List("**/generated/**")
}
}Unspecified properties keep the builtin’s values. Assigning a new list replaces that property’s list; it does not automatically append to it.
Use raw strings for commands
Raw strings help when a command contains quotes or backslashes:
local json_check = new Step {
glob = "*.json"
check = #"jq -e '.' {{files}} >/dev/null"#
}For longer commands, use a multiline raw string:
local test = new Step {
check = #"""
echo "Running tests"
mise run test
"""#
}The closing delimiter determines indentation. Keep the body indented consistently.
Comments
// A comment
/* A multiline comment */
/// A documentation comment
local explanation = "Documentation comments describe the following declaration."Share configuration across files
amends "./hk.pkl"
hooks {
["check"] {
steps {
["local-check"] {
check = "make local-check"
}
}
}
}This is a local amendment of an existing project configuration. Save it as hk.local.pkl and keep it out of version control. The selected file amends hk.pkl; hk does not independently merge those two project files. See local overrides.
Validate and inspect
hk validate
hk check --planValidation evaluates the configuration without executing linter commands. A plan then shows how hk selects steps and files.
If the Pkl CLI is installed, inspect the evaluated module with:
pkl eval --format json hk.pklUse the Pkl language reference for features beyond these examples.
Evaluators
hk includes pklr and always uses it to evaluate project, local, and global configuration. The standalone Pkl CLI remains useful for inspecting modules, but it is not an hk runtime dependency or fallback evaluator.
Caching
The built-in evaluator persists downloaded packages and seeds the cache with the Pkl package matching the running hk version. Use HK_PKL_OFFLINE to require cached or embedded packages without network access.
Release builds cache evaluated configuration; debug builds disable this cache by default. When diagnosing an unexpected result after changing an import or evaluation input, bypass or clear the cache:
HK_CACHE=0 hk validate
hk cache clearUse hk’s runtime settings, profiles, and command environment where possible instead of making configuration depend on changing evaluation inputs.
