docs/proposals/concurrency.md
This document outlines the design for parallel hook execution using explicit priority levels.
By default, prek executes hooks sequentially. While safe, this is inefficient for independent tasks (e.g., linting different languages). This proposal introduces per-hook priorities to allow safe, parallel execution of hooks.
priorityA new optional field priority is added to the hook configuration.
- id: cargo-fmt
priority: 10
u32None (auto-populated by hook index)When priority is omitted, the scheduler assigns the hook a priority equal to its index in the configuration file, starting at 0. This preserves the current sequential behavior by giving each hook a unique, increasing priority by default.
Execution is driven purely by priority numbers:
priority is global within a single configuration file. That is, priorities are compared across all hooks in the same .pre-commit-config.yaml, even if the hooks live under different repos: entries.
priority does not apply across different .pre-commit-config.yaml files (or separate prek runs with different configs). Each config file is scheduled independently.
require_serial ClarificationThe existing require_serial configuration key often causes confusion. In this design, its meaning is strictly scoped:
require_serial: true: Controls invocation concurrency for that hook. When running a hook against files, prek limits that hook to a single in-flight invocation at a time. This effectively disables running multiple batches of the same hook concurrently.
prek will still try to pass all files in one invocation, but may split into multiple invocations if the OS command-line length limit would be exceeded.require_serial: true can still run in parallel with other hooks that share its priority.Implicit priorities are always derived from the hook's position in the configuration (0-based), regardless of any explicitly configured priorities on other hooks.
Positions are taken from the fully flattened hook list for the current .pre-commit-config.yaml, in the order hooks appear as repos: and hooks: are read. In other words, implicit priorities are assigned across the whole file, not per-repo.
Example:
0 with no priority gets implicit priority 0.1 with priority: 10 keeps priority 10.2 with no priority gets implicit priority 2.This means a later hook with an implicit priority can run before an earlier hook that was assigned a larger explicit priority.
If you want to avoid surprises when introducing explicit priorities, prefer setting priority on all hooks (or at least on every hook whose relative order matters).
If files are modified during a parallel priority group, prek can only tell that one or more hooks in the group made changes (not which one). In this case, prek prints a grouped tree for the whole priority group and marks the group as failed.
Example:
Files were modified by following hooks...................................Failed
┌ Modifies File........................................................Passed
│ Prints Output........................................................Passed
└ No Output............................................................Passed
Later Hook...............................................................Passed
If fail_fast is enabled:
prek should wait for currently running hooks with the current priority to finish, but abort the execution of higher-priority groups.repos:
- repo: local
hooks:
- id: cargo-fmt
name: Format Rust
entry: cargo fmt
language: system
priority: 0 # Runs first
# These hooks are in different repos, but share the same priority,
# so they can run concurrently.
- repo: local
hooks:
- id: ruff
name: Lint Python
entry: ruff check
language: system
priority: 10
- repo: local
hooks:
- id: shellcheck
name: Lint Shell
entry: shellcheck
language: system
priority: 10
- repo: local
hooks:
- id: integration-tests
name: Integration Tests
entry: just test
language: system
priority: 20 # Starts after priority=10 group completes