Monorepo
Organize frontend, backend, and infrastructure checks into groups, then add repository-wide Markdown and YAML checks.
Prerequisites: the tools referenced in the configuration must be available, with project configuration in the appropriate directories. This example expects frontend/, backend/, and infrastructure/.
Download monorepo.pkl and save it as hk.pkl.
Configuration
/// Example configuration for a monorepo with multiple languages
/// Frontend: JavaScript/TypeScript with React
/// Backend: Rust
/// Infrastructure: Terraform
/// Uses groups to organize steps by component
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"
// Frontend linters (JavaScript/TypeScript)
local frontend = new Group {
// Inherited by frontend steps unless a child overrides `dir`.
dir = "frontend"
steps {
["prettier"] = (Builtins.prettier) {
batch = true
}
["eslint"] = (Builtins.eslint) {
batch = true
}
["stylelint"] = (Builtins.stylelint) {
// Override the group dir for a step that scans files from the repo root.
dir = "."
glob = List("frontend/**/*.css", "frontend/**/*.scss", "packages/design-system/**/*.scss")
}
}
}
// Backend linters (Rust)
local backend = new Group {
// Inherited by all backend steps.
dir = "backend"
workspace_indicator = "Cargo.toml"
steps {
["cargo_fmt"] = Builtins.cargo_fmt
["cargo_clippy"] = Builtins.cargo_clippy
["cargo_check"] = (Builtins.cargo_check) {
// Enable explicitly with --profile slow.
profiles = List("slow")
}
}
}
// Infrastructure linters (Terraform)
local infrastructure = new Group {
dir = "infrastructure"
exclude = List("**/.terraform/**")
steps {
["terraform"] = (Builtins.terraform) {
glob = "**/*.tf"
}
["tflint"] = (Builtins.tf_lint) {
glob = "**/*.tf"
// Child exclude replaces the group exclude, so repeat common exclusions.
exclude = List("**/.terraform/**", "modules/vendor/**")
}
}
}
// Shared linters (apply to all components)
local shared = new Mapping<String, Step> {
["markdown"] = (Builtins.markdown_lint) {
glob = List("**/*.md")
exclude = List("**/node_modules/**", "**/target/**")
}
["yaml"] = (Builtins.yamllint) {
glob = List("**/*.yaml", "**/*.yml")
exclude = List("**/node_modules/**")
}
}
hooks {
["pre-commit"] {
fix = true
stash = "git"
steps {
["frontend"] = frontend
["backend"] = backend
["infrastructure"] = infrastructure
...shared
}
}
["check"] {
steps {
["frontend"] = frontend
["backend"] = backend
["infrastructure"] = infrastructure
...shared
}
}
}Understand the boundaries
Each group can provide common defaults such as dir, prefix, and workspace_indicator. A child keeps an explicitly defined property; child values replace rather than merge with group values. Builtins may already define these properties.
Groups also affect scheduling: children can run concurrently within a group, but groups run in order. If frontend and backend checks should overlap, place the steps in one mapping and use depends only where ordering is required.
Try it
hk validate
hk check --all --plan
hk check --all
hk check --all --profile slowThe slow profile enables the additional Cargo check. It is not enabled automatically in CI.
Adapt it
Change dir values to match your repository, remove components you do not use, and inspect --plan to verify how paths and workspaces are selected. Use hk check --why <step> when a component is unexpectedly skipped.
For tools that discover nested packages, see workspaces.
Nested configs with subprojects
Instead of describing every component in the root hk.pkl, each subproject can own its own hk.pkl next to its code. The root config lists the subproject directories (literals or globs):
// hk.pkl (repo root)
amends "package://github.com/jdx/hk/releases/download/v2.0.1/hk@2.0.1#/Config.pkl"
subprojects = List("frontend", "backend", "packages/*")
hooks {
["check"] {}
["pre-commit"] {
fix = true
stash = "git"
}
}// frontend/hk.pkl
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"
local linters = new Mapping<String, Step> {
// aube resolves these executables from frontend/node_modules/.bin
["eslint"] = (Builtins.eslint) {
prefix = List("aube", "exec")
}
["prettier"] = (Builtins.prettier) {
prefix = List("aube", "exec")
}
}
hooks {
["check"] {
steps = linters
}
// Hooks compose by name, so list the steps again for pre-commit.
["pre-commit"] {
steps = linters
}
}// backend/hk.pkl
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"
local linters = new Mapping<String, Step> {
["cargo-fmt"] = Builtins.cargo_fmt
["cargo-clippy"] = Builtins.cargo_clippy
}
hooks {
["check"] { steps = linters }
["pre-commit"] { steps = linters }
}The matching mise configuration makes hk, aube, and each component's tools available in the directory where its steps run:
# mise.toml (repo root)
monorepo_root = true
[monorepo]
config_roots = [".", "frontend", "backend"]
[tools]
aube = "latest"
hk = "latest"
[env]
HK_MISE = 1# frontend/mise.toml
[tools]
node = "lts"# backend/mise.toml
[tools]
rust = "stable"On Git 2.54+, prefer installing the mise-aware launcher once per developer machine with hk install --global --mise. For a repository-scoped installation on any supported Git version, use hk install --mise.
When hk runs from the repo root, each subproject's hooks are merged in, scoped to its directory:
- Step working directories and glob matching are relative to the subdirectory, so
frontend/hk.pklonly sees files underfrontend/. - Step names are prefixed with the directory (e.g.
frontend:eslint), which is the name to use with--steporskip_steps. - A subproject's
envapplies to its own steps only. - Glob entries like
packages/*match any directory containing an hk config file; directories without one are skipped. - Hooks compose by name. Steps declared only under
checkdo not automatically run underpre-commitorfix. - Define hook-wide settings such as
fix,stash,stage, andreportin the root config so every subproject uses the same behavior. - Only one level of subprojects is supported.
This maps directly onto mise monorepo config roots: the same directories that own a mise.toml can own their hk.pkl.
Use hk check --all --plan to inspect the resolved jobs without executing them. For this example, the plan includes frontend:eslint, frontend:prettier, backend:cargo-fmt, and backend:cargo-clippy.
