v0.2.2
Released on 2026-04-12. Full changelog
New Features
Section titled “New Features”required_runok_version field for version guards (#299)
Section titled “required_runok_version field for version guards (#299)”Every config and preset file can now declare a required_runok_version — a semver requirement expression such as ">=0.3.0" or ">=0.3, <0.5". When runok loads a file whose constraint is not satisfied by the running binary, loading fails with an error that names the exact file and the constraint, rather than silently ignoring newer schema fields.
required_runok_version: '>=0.3.0'definitions: flag_groups: field-flag: ['-f', '--field']The check runs per file, so the project runok.yml, any file pulled in via extends, and every transitively extended preset are all validated independently.
runok update-presets now respects this field when choosing upgrade tags. Candidate tags are inspected from newest to oldest, and the newest candidate whose preset tree (including transitive extends) satisfies the current runok binary is adopted. This lets preset repositories ship schema-incompatible changes under newer tags without breaking users who are still on older runok. When one or more newer candidates are skipped, update-presets emits a warning so you know that upgrading the runok binary would unlock newer preset versions.
Automatic preset refresh (the TTL-driven background update that runs inside runok check, runok exec, and similar commands) now applies the same check. The cache working tree is only advanced to a new revision after required_runok_version has been verified via git show, so concurrent runok processes never see a partially-updated preset that is too new for them. If the new revision is incompatible the refresh is silently skipped and the existing cached preset is used — the warning is reserved for update-presets so that normal operations stay quiet.
Nightly builds (X.Y.Z-nightly+<sha>) are treated as “latest” for the purpose of version checks, so any >=X.Y.Z constraint passes automatically. Upper-bounded ranges still reject nightly intentionally.
See Extends — Version Guards for preset authoring guidance and Configuration Schema — required_runok_version for the field reference.
Flag alias groups with <flag:name> placeholder (#278)
Section titled “Flag alias groups with <flag:name> placeholder (#278)”when clauses can now inspect every value of a repeated or aliased flag through the new <flag:name> placeholder and the corresponding flag_groups CEL variable.
Define a group of aliased flags under definitions.flag_groups, reference it with <flag:name> in a pattern, and then iterate through every captured value in the when clause:
definitions: flag_groups: field-flag: ['-f', '-F', '--field', '--raw-field']
rules: # Allow `gh api graphql` queries, but ask before any mutation. - allow: 'gh api graphql <flag:field-flag> *' when: '!flag_groups["field-flag"].exists(v, v.startsWith("query=mutation"))' - ask: 'gh api graphql <flag:field-flag> *'flag_groups[name] is always exposed as a list, even for a single occurrence, so you can use CEL list macros (exists, all, size) without juggling string-vs-list types. Every group declared in definitions.flag_groups is also present in the CEL variable as an empty list when the matched rule did not capture any value, so flag_groups["name"] never fails with an undeclared-reference error.
This unlocks several common security checks that were previously awkward or impossible:
gh api graphql— distinguish queries from mutations across-f,-F,--field,--raw-field.curl --data ...— detect attempts to send sensitive files (-d @/etc/passwd) across all-d/--data/--data-raw/--data-binaryaliases.docker run -v ...— inspect every--volumemount, not just the first one.git -c key=value ...— check every-c/--configoverride at once.
See <flag:name> and When Clauses — flag_groups for details.
Bug Fixes
Section titled “Bug Fixes”Wrapper recognition for subshell-wrapped compound commands (#297)
Section titled “Wrapper recognition for subshell-wrapped compound commands (#297)”Commands of the form <wrapper> (<compound>) — for example time (lefthook run pre-commit 2>&1 | tail -40) — are now recognized by wrapper patterns such as time <cmd>. The subshell is captured as a single <cmd> argument, then its body is split into sub-commands (lefthook run pre-commit, tail -40) and each is evaluated individually with Explicit Deny Wins.
Previously the wrapper path flattened the input to whitespace-delimited tokens, so time (ls | tail -40) came out as time, (ls, |, tail, -40) and matched neither the time <cmd> wrapper nor the compound-command path, falling through to defaults.action. Rule evaluation now uses a tree-sitter-bash walk to tokenize single commands, preserving shell groupings ((...), $(...), `...`, <(...)) as one token each so wrapper placeholder extraction can capture them whole. In the same fix, the compound extractor also recurses into bare subshells attached to a command, which keeps time (...) symmetric with the long-standing handling of echo $(...).