docs/proposals/hook-groups.md
This document outlines the design for selecting hooks by user-defined hook groups.
prek run --all-files already works in CI, agent workflows, and other
explicit command-line contexts. However, users sometimes need to run only a
project-specific subset of hooks:
Today, users can approximate this with hook stages, for example by adding
manual to hooks and running prek run --stage manual. That is confusing:
stages describe Git hook contexts, while these use cases describe arbitrary
execution profiles. Adding dedicated stages such as ci, docker, release,
or agent would repeat the same problem and require a growing vocabulary.
The goal is to add a small hook-level tagging mechanism that lets users define their own run profiles without changing Git hook stage semantics.
groupsA new optional field groups is added to project hook configuration.
repos:
- repo: local
hooks:
- id: format
name: Format Python
entry: ruff format
language: system
groups: ["format", "ci"]
- id: lint
name: Lint Python
entry: ruff check
language: system
groups: ["lint", "ci"]
- id: typecheck
name: Typecheck Python
entry: pyright
language: system
groups: ["slow", "agent"]
groups is a prek-only field. It is not part of upstream pre-commit, and it
should not be treated as remote hook manifest metadata. Groups describe how the
current project wants to run hooks, not an intrinsic property of a hook
repository. If a remote hook manifest contains groups, prek should warn and
ignore the field, the same way it treats other config-only hook fields such as
priority.
For prek.toml, use the same field name:
[[repos]]
repo = "local"
hooks = [
{
id = "format",
name = "Format Python",
entry = "ruff format",
language = "system",
groups = ["format", "ci"],
},
]
Add two repeatable options to prek run:
prek run --group <name>
prek run --no-group <name>
--group <name> is an include filter. When one or more groups are requested, a
hook is selected if its groups contains at least one requested group.
--no-group <name> is an exclude filter. A hook is removed if its groups
contains any excluded group.
If both options are provided, exclusion wins:
--group, if any --group values were provided.--no-group, if any --no-group values were provided.Examples:
prek run --all-files --group ci
prek run --all-files --group lint --group typecheck
prek run --all-files --no-group format
prek run --all-files --group ci --no-group slow
prek run --all-files --group ci --stage pre-push
Group filtering composes with existing project and hook selectors. The effective selection order should be:
--skip selectors and skip environment variables.--stage filtering, if provided.--stage were provided, apply the
existing default pre-commit stage filtering and hook-target manual
fallback.For example:
prek run frontend/ --group ci --no-group slow
This runs hooks in frontend/ that are tagged with ci, except hooks also
tagged with slow.
Hooks excluded by group filtering must not be installed or executed for that run. This matters for hooks that require large toolchains, unsupported local dependencies, or ecosystems the user intentionally does not want to invoke.
Groups are independent from Git hook stages.
When --group and --no-group are not used, the existing stage behavior is
unchanged: omitting --stage/--hook-stage first selects hooks eligible for
pre-commit. If no hook is selected and the command named hook IDs, those same
IDs are matched again against hooks configured for manual.
When --group or --no-group is used without an explicit --stage, prek run
enters group selection mode:
manual is not
used.prek run file mode: explicit
--files/--directory, --all-files, merge conflicts, or staged files.commit-msg and/or prepare-commit-msg cannot run
in this mode because they require Git's message file argument, so they are
filtered out.If this message-file filtering removes every hook matched by the group filters,
prek run should warn and fail instead of silently succeeding.
This works because manual prek run --group ... has no Git hook payload. All
non-message-file stages can be executed with file input or no filenames
according to each hook's normal filters and pass_filenames setting; only the
message-file stages need input that cannot be inferred and are not selected by
stage-less group runs.
When --group or --no-group is combined with explicit stage selection, the
filters compose by intersection:
prek run --group ci --stage pre-push
This runs hooks that are both tagged ci and eligible for pre-push.
Equivalent --hook-stage spelling should behave the same way.
This keeps group-based CI usage simple because prek run --group ci does not
require users to add manual to every CI hook. It also lets users narrow a
group to a real Git hook context when that is what they intend.
Groups only decide which hooks are eligible to run. After that, existing hook behavior remains unchanged:
files, exclude, types, types_or, and exclude_types still filter
files.always_run only applies after a hook survives group filtering.pass_filenames: false remains a hook execution setting, not a group
selection setting.priority continues to schedule the remaining hooks.fail_fast, require_serial, diff detection, modified-file reporting,
output handling, and hook result semantics are unchanged.If group filtering leaves no hooks to run, prek run should report that no
hooks matched the requested selectors and return failure, matching the behavior
for explicit selector mistakes.
In workspace mode, group names are CLI selectors applied across all selected projects:
prek run --group ci selects every hook tagged ci in every selected
project.Group matching is evaluated per hook. A project with no matching hooks is simply excluded from the run.
Hooks with omitted groups or groups: [] are ungrouped.
If no group options are passed, ungrouped hooks run as they do today.
If --group <name> is passed, ungrouped hooks do not match and are not run.
This proposal does not add a virtual ungrouped group.
If only --no-group <name> is passed, ungrouped hooks remain selected, because
they do not belong to the excluded group.
A hook may belong to multiple groups:
- id: ruff
groups: ["lint", "python", "ci"]
The hook matches any include group and is excluded by any exclude group:
--group lint selects it.--group ci --no-group python excludes it.--no-group format does not exclude it.Duplicate group names on the same hook should be treated as a single group membership. Implementations may deduplicate during parsing or matching.
groups: ["ci", "ci"]
Group names should be non-empty strings and must not contain whitespace. Names are matched exactly, so implementations should not trim or normalize group names before validation.
Invalid examples:
groups: ["", "ci slow", " ci", "ci\nslow"]
No fixed vocabulary should be enforced. In particular, ci, agent, slow,
format, and lint are examples, not reserved names.
Group matching should be case-sensitive. CI and ci are distinct groups.
This avoids platform-specific normalization surprises and matches most existing configuration key/value behavior.
If --group does-not-exist matches no hooks, the run should fail with the same
kind of explicit-selection error used for unmatched hook selectors.
If multiple groups are requested and at least one matches, unmatched group names should produce a warning rather than failing the entire run, consistent with the existing selector reporting style.
For example:
prek run --group ci --group does-not-exist
should run ci hooks and warn that does-not-exist matched no hooks.
--skip continues to remove hooks even if they match a group:
prek run --group ci --skip ruff
The ruff hook is skipped.
Existing skip environment variables continue to apply. They should not gain a new group syntax in this proposal.
Adding environment-level group selection such as PREK_GROUP or
PREK_NO_GROUP can be discussed separately if needed.
Groups apply equally to local, remote, meta, and builtin hooks when the project configuration can attach hook options to them.
Remote hook manifests should not define default groups. A remote repository
does not know the consuming project's CI, agent, or local workflow policy. If a
manifest contains groups, the field is ignored with a warning.
groups are selectors. They are not scheduler groups.
After group filtering, the remaining hooks are scheduled by existing priority
semantics. Hooks with the same priority may still run in parallel. Hooks in
the same groups value do not gain any ordering or concurrency relationship.
Group selection does not change modified-file detection. If selected hooks modify files, existing failure and diff reporting behavior applies.
If a hook is excluded by group filtering, file modifications that hook would
have made obviously cannot occur, and prek should not install or execute it.
prek try-repo should not accept --group or --no-group.
try-repo builds a temporary project configuration from the remote hook
manifest. That generated configuration contains hook ids, but it does not have
project-local groups metadata. Accepting group filters would make the command
appear to support group selection while every generated hook is effectively
ungrouped. Instead, these flags should be rejected by the CLI.
This proposal does not require changes to prek list.
It would be useful for prek list --output-format=json to include groups once
the field exists, but filtering prek list by group can be a follow-up.
This proposal should not add prek install --group.
Persisting group choices into installed Git shims would make it hard to inspect what actually runs from a normal Git hook. Installed hooks should keep using stage semantics.
Users who want profile-based execution can invoke prek run --group ...
explicitly in CI, agent workflows, or contributor documentation.
Future proposals may discuss install-time groups or default groups, but that changes the meaning of what runs by default and should be designed separately.
This proposal does not add:
ci stage.priority remains the scheduling mechanism.after dependencies.--output-format=grouped.ungrouped selector.prek install --group.prek try-repo --group or prek try-repo --no-group.If no groups, --group, or --no-group is used, behavior is unchanged.
Existing stages behavior is unchanged for normal prek run and installed Git
hook execution. The only new behavior is explicit group selection mode and the
ability to intersect that mode with an explicitly requested stage.
Existing configs that contain an unknown groups key currently ignore it. Once
this proposal is implemented, that key becomes meaningful. This is acceptable
because it is an additive feature and unknown keys are not part of a guaranteed
stable behavior contract.
At a high level, implementation should:
groups to config hook types and the built Hook type.--group and --no-group arguments to prek run.--stage was provided.--group and --no-group unsupported for prek try-repo.