## File: README.md ## About prek is a framework for running hooks on your code. It runs them before you commit changes, on demand, or in CI. These hooks can format files, catch lint errors, detect secrets, or run any other command your project defines. prek also installs and manages the tools and dependencies they need. You may already be familiar with the [pre-commit](https://pre-commit.com/) tool. prek is a reimagined version of it, built in Rust. It is faster and distributed as a single binary with no runtime dependencies. It is fully compatible with pre-commit configurations and hooks, so you can use it as a drop-in replacement without changing your setup. Although prek is pretty new, it’s already powering real‑world projects like [CPython](https://github.com/python/cpython), [Apache Airflow](https://github.com/apache/airflow), [FastAPI](https://github.com/fastapi/fastapi), and more projects are picking it up—see [Who is using prek?](#who-is-using-prek). If you’re looking for an alternative to `pre-commit`, please give it a try—we’d love your feedback! ## Features - A single binary with no dependencies, does not require Python or any other runtime. - [Faster](https://prek.j178.dev/benchmark/) than `pre-commit` and more efficient in disk space usage. - Fully compatible with the original pre-commit configurations and hooks. - Built-in support for monorepos (i.e. [workspace mode](https://prek.j178.dev/workspace/)), including concurrent execution for independent same-depth projects. - Integration with [`uv`](https://github.com/astral-sh/uv) for managing Python virtual environments and dependencies. - Improved toolchain installations for Python, Node.js, Bun, Go, Rust and Ruby, shared between hooks. - [Built-in](https://prek.j178.dev/builtin/) Rust-native implementation of some common hooks. ## Installation Standalone installer prek provides a standalone installer script to download and install the tool, On Linux and macOS: ```bash curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/download/v0.4.14/prek-installer.sh | sh ``` On Windows: ```powershell powershell -ExecutionPolicy ByPass -c "irm https://github.com/j178/prek/releases/download/v0.4.14/prek-installer.ps1 | iex" ``` PyPI prek is published as Python binary wheel to PyPI, you can install it using `pip`, `uv` (recommended), or `pipx`: ```bash # Using uv (recommended) uv tool install prek # Using uvx (install and run in one command) uvx prek # Adding prek to the project dev-dependencies uv add --dev prek # Using pip pip install prek # Using pipx pipx install prek ``` Homebrew ```bash brew install prek ``` mise To use prek with [mise](https://mise.jdx.dev) ([v2025.8.11](https://github.com/jdx/mise/releases/tag/v2025.8.11) or later): ```bash mise use prek ``` Cargo binstall Install pre-compiled binaries from GitHub using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall): ```bash cargo binstall prek ``` Cargo Build from source using Cargo (Rust 1.95+ is required): ```bash cargo install --locked prek ``` npmjs prek is published as a [Node.js package](https://www.npmjs.com/package/@j178/prek) and can be installed with any npm-compatible package manager: ```bash # As a dev dependency npm add -D @j178/prek pnpm add -D @j178/prek bun add -D @j178/prek # Or install globally npm install @j178/prek pnpm add @j178/prek bun install -g @j178/prek # Or run directly without installing npx @j178/prek --version bunx @j178/prek --version ``` Nix prek is available via [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=prek&query=prek). ```shell # Choose what's appropriate for your use case. # One-off in a shell: nix-shell -p prek # NixOS or non-NixOS without flakes: nix-env -iA nixos.prek # Non-NixOS with flakes: nix profile install nixpkgs#prek ``` Conda prek is available as `prek` via [conda-forge](https://anaconda.org/conda-forge/prek). ```shell conda install conda-forge::prek ``` Scoop (Windows) prek is available via [Scoop](https://scoop.sh/#/apps?q=prek). ```powershell scoop install main/prek ``` Winget (Windows) prek is available via [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/). ```powershell winget install --id j178.Prek ``` MacPorts prek is available via [MacPorts](https://ports.macports.org/port/prek/). ```bash sudo port install prek ``` GitHub Releases Pre-built binaries are available for download from the [GitHub releases](https://github.com/j178/prek/releases) page. GitHub Actions prek can be used in GitHub Actions via the [j178/prek-action](https://github.com/j178/prek-action) repository. Example workflow: ```yaml name: Prek checks on: [push, pull_request] jobs: prek: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 ``` This action installs prek and runs `prek run --all-files` on your repository. prek is also available via [`taiki-e/install-action`](https://github.com/taiki-e/install-action) for installing various tools. prek skill for agents To let agents use `prek`, install the `prek` skill with `gh skill` (`v2.90.0+`): ```bash gh skill install j178/prek prek ``` If installed via the standalone installer, prek can update itself to the latest version: ```bash prek self update ``` ## Quick start - **I already use pre-commit:** follow the short migration checklist in the [quickstart guide](https://prek.j178.dev/quickstart/#already-using-pre-commit) to swap in `prek` safely. - **I'm new to pre-commit-style tools:** learn the basics—creating a config, running hooks, and installing Git shims—in the [beginner quickstart walkthrough](https://prek.j178.dev/quickstart/#new-to-pre-commit-style-workflows). ## Why prek? ### prek is faster - It is [multiple times faster](https://prek.j178.dev/benchmark/) than `pre-commit` while also using less disk space. - Hook environments and toolchains are shared across hooks instead of being duplicated per repository, which reduces both install time and cache size. - Repository fetches and independent hook environment setup run in parallel, hooks can run concurrently by [`priority`](https://prek.j178.dev/reference/configuration/#priority) using reusable [aliases](https://prek.j178.dev/reference/configuration/#priorities), and independent workspace projects at the same directory depth can run concurrently. - It uses [`uv`](https://github.com/astral-sh/uv) for creating Python virtualenvs and installing dependencies, which is known for its speed and efficiency. - For supported hooks from `pre-commit-hooks`, the [automatic fast path](https://prek.j178.dev/builtin/#1-automatic-fast-path) runs built-in Rust implementations without requiring any configuration changes. - The prek-only `repo: builtin` mode provides offline, zero-setup hooks, including native `deny-pattern` and `require-pattern` alternatives for common `pygrep` checks. ### prek is easier to work with - No need to install Python or any other runtime just to use `prek`; it is a single binary. - Its [language support](https://prek.j178.dev/languages/) covers every language available in `pre-commit`, plus Bun, Deno, mise, and PHP, and it automatically installs managed toolchains when needed for Python, Node.js, Bun, Deno, Go, mise, Rust, and Ruby. - It supports native [`prek.toml`](https://prek.j178.dev/configuration/) in addition to pre-commit YAML, and [`prek util yaml-to-toml`](https://prek.j178.dev/reference/cli/#prek-util-yaml-to-toml) helps migrate existing configs. - Built-in support for [workspaces](https://prek.j178.dev/workspace/) means monorepos can keep separate configs per project and still run everything from one command, while independent same-depth projects run concurrently without mixing file scopes. - [`prek install`](https://prek.j178.dev/reference/cli/#prek-install) and [`prek uninstall`](https://prek.j178.dev/reference/cli/#prek-uninstall) honor repo-local and worktree-local `core.hooksPath`. - Hook [`groups`](https://prek.j178.dev/reference/configuration/#groups) let one config define workflows such as CI, linting, or formatting; `--group`, `--require-group`, and `--no-group` select them at runtime. - [`prek run`](https://prek.j178.dev/reference/cli/#prek-run) can select or skip multiple projects and hooks, target tracked files with repeatable `--glob` or `--directory` filters, pass explicit paths with `--files`, and preview the selection with `--dry-run`. - The progress UI streams a live preview from running hooks, so long-running checks do not look stuck and failures are easier to diagnose. - [`prek list`](https://prek.j178.dev/reference/cli/#prek-list), [`prek util identify`](https://prek.j178.dev/reference/cli/#prek-util-identify), and [`prek util list-builtins -v`](https://prek.j178.dev/reference/cli/#prek-util-list-builtins) make it easier to inspect configured hooks, debug file matching, and discover builtins with their supported options. ### prek includes security-focused safeguards - For supported managed toolchain downloads, `prek` verifies the downloaded archive or installer checksum before extracting or installing it, helping ensure the integrity of downloaded toolchains. - [`prek update`](https://prek.j178.dev/reference/cli/#prek-update) can keep newly published releases on hold with `--cooldown-days`, filter eligible tags with glob patterns, and freeze revisions to commit SHAs. - [`prek update`](https://prek.j178.dev/reference/cli/#prek-update) validates pinned SHA revisions against the fetched upstream refs, including impostor-commit detection, and keeps `# frozen:` comments in sync with the configured commit. - [`prek update --check`](https://prek.j178.dev/reference/cli/#prek-update--check) is useful in CI when you want updates or frozen-reference mismatches to fail the job without rewriting the config. For more detailed improvements prek offers, take a look at [Difference from pre-commit](https://prek.j178.dev/diff/). ## Who is using prek? prek is pretty new, but it is already being used or recommended by some projects and organizations. GitHub stars are current as of April 15, 2026. - [apache/airflow](https://github.com/apache/airflow/issues/44995) 45,050 stars - [apache/iggy](https://github.com/apache/iggy/pull/2383) 4,116 stars - [apache/lucene](https://github.com/apache/lucene/pull/15629) 3,401 stars - [ast-grep/ast-grep](https://github.com/ast-grep/ast-grep.github.io/commit/e30818144b2967a7f9172c8cf2f4596bba219bf5) 13,413 stars - [astral-sh/ruff](https://github.com/astral-sh/ruff/pull/22505) 47,070 stars - [astral-sh/ty](https://github.com/astral-sh/ty/pull/2469) 18,308 stars - [authlib/authlib](https://github.com/authlib/authlib/pull/804) 5,271 stars - [cachix/devenv](https://github.com/cachix/devenv/pull/2304) 6,665 stars - [cocoindex-io/cocoindex](https://github.com/cocoindex-io/cocoindex/pull/1564) 6,865 stars - [commitizen-tools/commitizen](https://github.com/commitizen-tools/commitizen) 3,377 stars - [DetachHead/basedpyright](https://github.com/DetachHead/basedpyright/pull/1413) 3,267 stars - [django/djangoproject.com](https://github.com/django/djangoproject.com/pull/2252) 1,994 stars - [fastapi/asyncer](https://github.com/fastapi/asyncer/pull/437) 2,407 stars - [fastapi/fastapi](https://github.com/fastapi/fastapi/pull/14572) 97,209 stars - [fastapi/typer](https://github.com/fastapi/typer/pull/1453) 19,210 stars - [Future-House/paper-qa](https://github.com/Future-House/paper-qa/pull/1098) 8,377 stars - [getsentry/sentry](https://github.com/getsentry/sentry/pull/110808) 43,639 stars - [godotengine/godot](https://github.com/godotengine/godot/pull/119150) 110,312 stars - [home-assistant/core](https://github.com/home-assistant/core/pull/160427) 86,029 stars - [jcrist/msgspec](https://github.com/jcrist/msgspec/pull/918) 3,692 stars - [jlowin/fastmcp](https://github.com/jlowin/fastmcp/pull/2309) 24,539 stars - [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli/pull/535) 7,800 stars - [openclaw/openclaw](https://github.com/openclaw/openclaw/pull/1720) 357,512 stars - [OpenLineage/OpenLineage](https://github.com/OpenLineage/OpenLineage/pull/3965) 2,406 stars - [pdm-project/pdm](https://github.com/pdm-project/pdm/pull/3593) 8,553 stars - [prowler-cloud/prowler](https://github.com/prowler-cloud/prowler/pull/10601) 13,592 stars - [pyodide/pyodide](https://github.com/pyodide/pyodide/pull/6182) 14,527 stars - [python-attrs/attrs](https://github.com/python-attrs/attrs/commit/c95b177682e76a63478d29d040f9cb36a8d31915) 5,770 stars - [python-telegram-bot/python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot/pull/5142) 29,025 stars - [python/cpython](https://github.com/python/cpython/issues/143148) 72,330 stars - [simple-icons/simple-icons](https://github.com/simple-icons/simple-icons/pull/14245) 24,873 stars For a more comprehensive list of open-source projects using prek see the [list of dependents on github](https://github.com/j178/prek/network/dependents). ## Acknowledgements This project is heavily inspired by the original [pre-commit](https://pre-commit.com/) tool, and it wouldn't be possible without the hard work of the maintainers and contributors of that project. And a special thanks to the [Astral](https://github.com/astral-sh) team for their remarkable projects, particularly [uv](https://github.com/astral-sh/uv), from which I've learned a lot on how to write efficient and idiomatic Rust code. --- ## File: docs/proposals/concurrency.md # Priority-based parallel hook execution This document outlines the design for parallel hook execution using explicit priority levels. ## Motivation 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. ## Configuration ### Hook Configuration: `priority` A new optional field `priority` is added to the hook configuration. ```yaml - id: cargo-fmt priority: 10 ``` - **Type**: `u32` - **Default**: `None` (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 Model Execution is driven purely by priority numbers: ### Scope `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. 1. **Ordering**: Hooks run from the lowest priority value to the highest. 2. **Concurrency**: Hooks that share the same priority execute concurrently, subject to `PREK_CONCURRENT_HOOKS` (default: number of CPUs). 3. **Defaults**: Without explicit priorities, each hook receives a unique priority derived from its position, so execution remains sequential and backwards-compatible. 4. **Conflicts**: If two hooks intentionally share a priority, they will be run in parallel. Users are responsible for assigning priorities that match their desired grouping. ## `require_serial` Clarification The existing `require_serial` configuration key often causes confusion. In this design, its meaning is strictly scoped: - **`require_serial: true`**: Controls **batch concurrency for that hook**. A batch is one hook command invocation over a subset of the matched filenames. When running a hook against files, `prek` limits that hook to a single in-flight batch 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. - **It does NOT imply exclusive execution**. A hook with `require_serial: true` can still run in parallel with other hooks that share its `priority`. - If a hook *must* run alone (e.g., it modifies global state), it should be assigned a unique priority value that no other hook uses. ## Design Considerations ### Mixing Explicit and Implicit Priorities 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: - Hook at index `0` with no `priority` gets implicit priority `0`. - Hook at index `1` with `priority: 10` keeps priority `10`. - Hook at index `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). ### Grouped Output 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 ``` ### Fail Fast If `fail_fast` is enabled: - If a hook fails, `prek` should wait for currently running hooks with the *current priority* to finish, but **abort** the execution of higher-priority groups. ### Example Configuration ```yaml 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 ``` --- ## File: docs/authoring-hooks.md # Authoring Hooks This page is for hook authors who publish a repository consumed by end users. If you only need to configure hooks in your own project, see [Configuration](configuration.md). ## Manifest file: `.pre-commit-hooks.yaml` Hook repositories must include a `.pre-commit-hooks.yaml` file at the repo root. There is no separate `prek` manifest format; `prek` reads the same `.pre-commit-hooks.yaml` manifest defined by upstream `pre-commit`. This keeps hook repositories compatible with the broader pre-commit ecosystem. Hooks should exit non-zero on failure (or modify files and exit non-zero for fixers). The manifest is a YAML list of hook definitions. `prek` supports these fields in each manifest hook: | Field | Required | `prek`-only | Type | Description | | -- | -- | -- | -- | -- | | `id` | Yes | No | string | Stable identifier used in end-user configs. | | `name` | Yes | No | string | Human-friendly label shown in output. | | `entry` | Yes | No | string | Command to execute. | | `shell` | No | Yes | string enum | Run `entry` through a predefined shell adapter (`sh`, `bash`, `pwsh`, `powershell`, or `cmd`). | | `language` | Yes | No | string | Execution environment, for example `python`, `node`, or `system`. | | `alias` | No | No | string | Alternate identifier accepted by `prek run`. | | `files` | No | No | regex string | Include only matching files. | | `exclude` | No | No | regex string | Exclude matching files. | | `types` | No | No | list of strings | Require all listed file type tags. | | `types_or` | No | No | list of strings | Require at least one listed file type tag. | | `exclude_types` | No | No | list of strings | Exclude files with any listed file type tag. | | `additional_dependencies` | No | No | list of strings | Extra dependencies installed into managed hook environments. | | `args` | No | No | list of strings | Extra arguments appended to `entry` before filenames. | | `env` | No | Yes | map of strings | Runtime environment variables for the hook process. | | `always_run` | No | No | boolean | Run even when no files match. | | `fail_fast` | No | No | boolean | Stop the run immediately if this hook fails. | | `pass_filenames` | No | No | boolean or positive integer | Control whether, or how many, matching filenames are passed. | | `description` | No | No | string | Free-form metadata shown in listings; its first line is also shown with run details. | | `language_version` | No | No | string | Language/toolchain version request. | | `log_file` | No | No | string path | Write hook output to a file when the hook fails or is verbose. | | `require_serial` | No | No | boolean | Avoid concurrent invocations of this hook. | | `stages` | No | No | list of stage names | Git hook stages where this hook is eligible to run. | | `verbose` | No | No | boolean | Print output even when the hook succeeds. | | `minimum_prek_version` | No | Yes | version string | Minimum `prek` version required for this hook. | For fields shared with upstream `pre-commit`, `prek` follows the upstream manifest semantics. For the upstream reference, see: [https://pre-commit.com/#new-hooks](https://pre-commit.com/#new-hooks). !!! note "`prek`-only manifest fields" `prek`-only fields are accepted by `prek`, but upstream `pre-commit` will not recognize them. End-user configuration may also set [`env`](reference/configuration.md#prek-only-env) and [`shell`](reference/configuration.md#shell). When both the manifest and end-user config define `env`, the maps are merged and end-user values override duplicate keys. `pass_filenames: n` with a positive integer is also a `prek` extension. Upstream `pre-commit` only accepts a boolean value. When `shell` is set, `entry` is treated as shell source. Hook `args` and filenames are passed as script arguments, so POSIX shell entries should read them with `"$@"`. `shell` is supported only for language backends that use the shell-aware entry resolver; see [`shell`](reference/configuration.md#shell) for the supported languages and exact shell adapter commands. !!! note "Manifest fields only" Project configuration-only fields, such as `priority` and `groups`, are not manifest hook fields. Example: ```yaml - id: format-json name: format json entry: python3 -m tools.format_json language: python files: "\\.json$" - id: lint-shell name: shellcheck entry: shellcheck language: system types: [shell] ``` ## Choosing hook stages Hook authors can declare which Git hook stages they support with `stages` in `.pre-commit-hooks.yaml`. End users can override that list in their configuration. If neither is set, `prek` falls back to the top-level `default_stages` (which defaults to all stages). The `manual` stage is special: it never runs automatically and is only executed when a user explicitly runs `prek run --hook-stage manual `. For what each stage means and whether it operates on repository files, see [Supported Git Hook Stages](reference/configuration.md#supported-git-hook-stages). Example: ```yaml - id: lint name: lint entry: my-lint language: python stages: [pre-commit, pre-merge-commit, pre-push, manual] ``` ## Passing arguments to hooks When users configure a hook with `args`, `prek` passes those arguments before the list of file paths. If `args` is empty or omitted, only file paths are provided. Example end-user config: ```yaml repos: - repo: https://github.com/example/hook-repo rev: v1.0.0 hooks: - id: my-hook args: [--max-line-length=120] ``` Invocation shape: ```text my-hook --max-line-length=120 path/to/file1 path/to/file2 ``` ## Versioning for `prek update` End users pin your repository using the `rev` field in their config. To make [`prek update`](reference/cli.md#prek-update) work as expected, publish git tags for releases: - Prefer semantic version tags like `v1.2.3` or `1.2.3`. - Push tags to the remote (annotated or lightweight tags both work). - Avoid moving tags; treat them as immutable release references. `prek update` selects the newest tag by default. With `--bleeding-edge`, it uses the default branch tip instead of tags. With `--freeze`, it writes commit SHAs into `rev` instead of tag names. ## Develop locally with `prek try-repo` [`prek try-repo`](reference/cli.md#prek-try-repo) runs hooks from a repository without publishing a release. This is handy while iterating on a hook. ```bash # In another repository where you want to test the hook prek try-repo ../path/to/hook-repo my-hook-id --verbose ``` Notes: - `prek try-repo` accepts any path or git URL `git clone` understands. - For `prepare-commit-msg` or `commit-msg` hooks, pass the appropriate `--commit-msg-filename` argument when testing. ## Validation and CI Validate your manifest locally with [`prek validate-manifest`](reference/cli.md#prek-validate-manifest): ```bash prek validate-manifest .pre-commit-hooks.yaml ``` This ensures the manifest is well-formed before publishing a release tag. --- ## File: docs/benchmark.md # Benchmarks "How much faster is prek?" sounds like a question that should have one simple answer. In practice, it depends on where the time goes. Some of that time belongs to the hook itself: a formatter reads files, a linter builds an analysis, or `cargo clippy` compiles a crate. The rest belongs to the runner around it. prek and pre-commit still need to load configuration, ask Git which files matter, match those files to hooks, start processes, present the results, and determine whether anything changed. That distinction is the key to reading these numbers. If `cargo clippy` takes 30 seconds, even cutting everything around it from roughly 1.4 seconds to 0.1 seconds only changes the total from 31.4 seconds to 30.1 seconds: about a 1.04x speedup. At the other extreme, a repository with many quick hooks can spend most of its time inside the runner, especially when checking the worktree with `git diff` is expensive. Rather than jumping straight to one headline number, we will build the picture from the smallest workload upward. First we will make the hook almost free and compare the frameworks themselves. Then we will bring back real hooks and add prek's fast path, priority scheduling, and project-level concurrency one step at a time. ## Start with the framework The simplest way to expose framework cost is to make the hook do almost nothing. Here every hook is a local `language: system` hook whose entry is `true`. `always_run: true` and `pass_filenames: false` ensure that every hook runs exactly once with the same tiny payload: ```yaml repos: - repo: local hooks: - id: noop-01 name: no-op 01 entry: "true" language: system always_run: true pass_filenames: false ``` The full configuration repeats this definition with unique IDs through `noop-10`. Both measurements load that same configuration; the one-hook case selects only `noop-01`, while the ten-hook case runs all hooks sequentially. These are generic hooks, so prek's built-in fast path is not involved. This is still not a measurement of literally zero hook cost: the runner must launch a child process for every `true`. The one-hook case mostly exposes fixed startup and repository-processing costs. With ten hooks, both runners repeat dispatch and modification checks; prek saves 203 ms in absolute time, while the relative gap narrows from 3.30x to 1.80x. That gives us a useful lower bound, but not yet a representative hook workload. The next question is what happens when the hooks perform real, predictable work and prek can apply its own runtime optimizations. ## Add optimizations one at a time Now we replace `true` with 13 unique hooks from [`pre-commit-hooks`](https://github.com/pre-commit/pre-commit-hooks). The medium-scale corpus remains the same: 960 workload files, a clean worktree, and warm hook environments and filesystem caches. Installation and network time are not included. To make each change visible, we begin with prek's fast path disabled and add one optimization at a time. Each stage keeps the improvements introduced before it, and we will look at its result before moving on to the next one. ### 1. No fast path We first set `PREK_NO_FAST_PATH=1`, so prek runs the original Python hook implementations. The hooks keep their implicit priorities and run sequentially. This gives us a baseline before enabling any prek-specific runtime optimizations. The `pre-commit` reference takes 1,737 ms. prek completes the same workload in 1,438 ms, 17% less time. We use that 1,438 ms result as the baseline for the remaining stages. ### 2. Fast path Next, we remove `PREK_NO_FAST_PATH` without changing the hook configuration. prek's [automatic fast path](builtin.md#1-automatic-fast-path) recognizes the 13 hooks and runs their built-in Rust implementations. The median falls from 1,438 ms to 213 ms: 85% less time, or a 6.75x speedup. This benchmark deliberately uses hooks for which prek has built-in implementations. That keeps the execution path predictable, but it also makes the fast path unusually visible. Treat this result as an explanation of where time can be removed, not as a promise for every workload. ### 3. Priority The fast path is automatic when a supported hook matches. Priority requires a little more information: you need to tell prek which hooks are independent and can safely run together. Hooks with the same [`priority`](reference/configuration.md#priority) may run at the same time. Only group hooks that are independent. In the benchmark, hooks which can modify overlapping text files remain ordered, and read-only checks share a named priority. This excerpt shows the grouping: ```yaml priorities: trim: 0 eof: 10 line-endings: 20 bom: 30 checks: 40 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace priority: trim - id: end-of-file-fixer priority: eof - id: mixed-line-ending priority: line-endings - id: fix-byte-order-marker priority: bom - id: check-json priority: checks - id: check-yaml priority: checks - id: check-toml priority: checks - id: check-xml priority: checks ``` With the fast path retained, priority scheduling lowers the median from 213 ms to 172 ms, another 19% reduction. That is 8.36x faster than the no-fast-path baseline. ### 4. Two projects Priority scheduling shortens the critical path inside one project. The final stage applies the same idea one level higher by splitting structured data and text files into two sibling projects: ```text benchmark-repo/ ├── .pre-commit-config.yaml ├── structured/ │ └── .pre-commit-config.yaml └── text/ └── .pre-commit-config.yaml ``` Each project declares only the hooks it owns. Common checks appear in both project configurations, but they see disjoint sets of files. This models a configured monorepo rather than duplicating every hook in every project just to exercise the scheduler. [Projects at the same depth](workspace.md#execution-order) can run concurrently. Nested parent and child projects still run from deepest to shallowest, so a workspace should reflect real ownership boundaries rather than being split only to chase a benchmark number. Adding the second project lowers the median from 172 ms to 135 ms, another 21% reduction. ### Summary Put together, the four measurements form the complete optimization ladder: Across the complete ladder, prek falls from 1,438 ms to 135 ms: 90.6% less time, or a 10.64x speedup. The final configuration is 12.85x faster than the `pre-commit` reference. ## The hidden cost of `git diff` The large fast-path jump is not only about replacing Python hook implementations with Rust. It also removes work around the hooks. An arbitrary hook can modify files without reporting that fact, so a hook manager needs another way to decide whether the worktree changed. That check is often `git diff`, and it can be a substantial part of framework time when a repository contains many files. In the versions measured here, `pre-commit` captures one diff before running the hooks and another after every hook that executes. Thirteen executed hooks can therefore require 14 diffs. prek also uses diffs when a general hook's mutation status is unknown, but its built-in and automatic fast-path hooks report whether they changed files. When every result is known, prek can skip those diffs entirely. This fixture is deliberately moderate: a separate 30-run check of a clean `git diff` had a median of about 18 ms. The difference becomes much larger in an extreme repository where one diff takes hundreds of milliseconds. For example, at 300 ms per diff: | Execution path | Diff calls for 13 hooks | Diff time alone | | -- | -: | -: | | `pre-commit` | 14 | about 4.2 s | | prek, all results known | 0 | 0 s | This is an illustration of diff overhead, not a claim that the hooks themselves take zero time. If even one hook has an unknown mutation outcome, prek still performs the required check rather than assuming the worktree is unchanged. ## What this means for a real repository At this point, two different kinds of speedup should be visible. Framework savings remove time around each hook, while concurrency shortens the critical path through the hooks themselves: - A single slow hook hides framework improvements. The total cannot become much faster than that hook. - Correct priority groups shorten the critical path. Three independent 20-second hooks can approach 20 seconds instead of 60 seconds when resources allow. - Independent same-depth projects can make the same improvement across a monorepo. Each project advances through its own priority groups without waiting for an unrelated sibling project. Well-configured priority and project concurrency can therefore produce multi-fold improvements even when fast-path savings are small. Actual scaling is bounded by CPU, memory, disk and cache contention, and by [`PREK_CONCURRENT_HOOKS`](reference/environment-variables.md#prek_concurrent_hooks). Do not run hooks concurrently when they modify the same files or share mutable global state. ## Reproduce the benchmark The complete fixture generator, pinned hook configurations, hyperfine commands, and raw samples are published in [`prek-ci/benchmarks`](https://github.com/prek-ci/benchmarks). The generator recreates all three fixture layouts and verifies their Git tree hashes, so a change to any workload file is detected before measurements begin. With Git, uv, hyperfine, and Python 3 installed, run: ```console git clone https://github.com/prek-ci/benchmarks.git cd benchmarks ./scripts/setup-tools.sh ./benchmark.sh ``` `setup-tools.sh` installs the prek 0.4.12 binary wheel and pre-commit 4.6.1 in isolated tool environments using Python 3.14.6. `benchmark.sh` creates a fresh 960-file fixture, warms both runner caches, executes the framework, runtime-ladder, and clean-`git diff` benchmarks in both command orders, and writes the raw JSON plus a pooled-median summary to `results/local-/`. The [original 2026-07-31 samples](https://github.com/prek-ci/benchmarks/tree/main/results/2026-07-31) are preserved alongside the scripts. Expect absolute times to vary across machines; compare the ordering and relative changes on your own hardware. ## Methodology - Date: 2026-07-31 - OS: macOS 15.7.7 - CPU: Apple M3 Pro, 12 cores - RAM: 18 GiB - prek: `0.4.12` - pre-commit: `4.6.1` - pre-commit-hooks: `v6.0.0` - hyperfine: `1.20.0` - Framework workload: 960 workload files; 10 local `language: system` hooks invoking `true`; `always_run: true`; `pass_filenames: false`; one or ten hooks selected; clean worktree and warm filesystem caches - Optimization workload: the same 960 files; configuration files excluded from hook matching; 13 unique built-in-capable hook IDs; clean worktree; warm hook environments and filesystem caches - Sampling: 5 warmups followed by 15 measured runs in forward order and 15 in reverse order; the charts report the pooled median of 30 runs Benchmark performance varies with hardware, operating system, repository shape, hook configuration, cache state, and background load. Compare representative end-to-end workloads on your own machine before making configuration decisions. --- ## File: docs/builtin.md # Built-in Fast Hooks prek includes fast, Rust-native implementations of popular hooks for speed and low overhead. These hooks are bundled directly into the `prek` binary, eliminating the need for external interpreters like Python for these specific checks. Built-in hooks come into play in two ways: 1. **Automatic Fast Path**: Automatically replacing execution for known remote repositories. 2. **Explicit Builtin Repository**: Using `repo: builtin` for offline, zero-setup hooks. ## 1. Automatic Fast Path When you use a standard configuration pointing to a supported repository (like `https://github.com/pre-commit/pre-commit-hooks`), `prek` automatically detects this and runs its internal Rust implementation instead of the Python version defined in the repository. The fast path is activated when the `repo` URL matches `https://github.com/pre-commit/pre-commit-hooks`. No need to change anything in your configuration. Note that the `rev` field is ignored for detection purposes. This provides a speed boost while keeping your configuration compatible with the original `pre-commit` tool. ```yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks # Enables fast path rev: v4.5.0 # This is ignored for fast path detection hooks: - id: trailing-whitespace ``` !!! note In this mode, `prek` will still clone the repository and create the environment (e.g., a Python venv) to ensure full compatibility and fallback capabilities. However, the actual hook execution bypasses the environment and runs the native Rust code. ### Supported Hooks Currently, only part of hooks from `https://github.com/pre-commit/pre-commit-hooks` is supported. More popular repositories may be added over time. ### - [`trailing-whitespace`](https://github.com/pre-commit/pre-commit-hooks#trailing-whitespace) (Trims trailing whitespace.) - [`check-added-large-files`](https://github.com/pre-commit/pre-commit-hooks#check-added-large-files) (Prevents giant files from being committed.) - [`check-case-conflict`](https://github.com/pre-commit/pre-commit-hooks#check-case-conflict) (Checks for files that would conflict in case-insensitive filesystems.) - [`check-illegal-windows-names`](https://github.com/pre-commit/pre-commit-hooks#check-illegal-windows-names) (Checks for filenames which cannot be created on Windows.) - [`end-of-file-fixer`](https://github.com/pre-commit/pre-commit-hooks#end-of-file-fixer) (Ensures that a file is either empty, or ends with one newline.) - [`file-contents-sorter`](https://github.com/pre-commit/pre-commit-hooks#file-contents-sorter) (Sorts the lines in specified files (defaults to alphabetical).) - [`requirements-txt-fixer`](https://github.com/pre-commit/pre-commit-hooks#requirements-txt-fixer) (Sorts entries in requirements.txt.) - [`fix-byte-order-marker`](https://github.com/pre-commit/pre-commit-hooks#fix-byte-order-marker) (Removes UTF-8 byte order marker.) - [`forbid-new-submodules`](https://github.com/pre-commit/pre-commit-hooks#forbid-new-submodules) (Prevents the addition of new Git submodules.) - [`check-json`](https://github.com/pre-commit/pre-commit-hooks#check-json) (Checks JSON files for parseable syntax.) - [`check-toml`](https://github.com/pre-commit/pre-commit-hooks#check-toml) (Checks TOML files for parseable syntax.) - [`check-vcs-permalinks`](https://github.com/pre-commit/pre-commit-hooks#check-vcs-permalinks) (Ensures that links to VCS websites are permalinks.) - [`check-yaml`](https://github.com/pre-commit/pre-commit-hooks#check-yaml) (Checks YAML files for parseable syntax.) - [`check-xml`](https://github.com/pre-commit/pre-commit-hooks#check-xml) (Checks XML files for parseable syntax.) - [`mixed-line-ending`](https://github.com/pre-commit/pre-commit-hooks#mixed-line-ending) (Replaces or checks mixed line endings.) - [`check-symlinks`](https://github.com/pre-commit/pre-commit-hooks#check-symlinks) (Checks for symlinks which do not point to anything.) - [`destroyed-symlinks`](https://github.com/pre-commit/pre-commit-hooks#destroyed-symlinks) (Detects symlinks that were replaced with regular files whose contents are the original symlink target path.) - [`check-merge-conflict`](https://github.com/pre-commit/pre-commit-hooks#check-merge-conflict) (Checks for files that contain merge conflict strings.) - [`detect-private-key`](https://github.com/pre-commit/pre-commit-hooks#detect-private-key) (Detects the presence of private keys.) - [`no-commit-to-branch`](https://github.com/pre-commit/pre-commit-hooks#no-commit-to-branch) (Protects specific branches from direct commits.) - [`check-shebang-scripts-are-executable`](https://github.com/pre-commit/pre-commit-hooks#check-shebang-scripts-are-executable) (Ensures that (non-binary) files with a shebang are executable.) - [`check-executables-have-shebangs`](https://github.com/pre-commit/pre-commit-hooks#check-executables-have-shebangs) (Ensures that (non-binary) executables have a shebang.) #### Notes - `check-yaml` fast path does not yet support the `--unsafe` flag; for those cases, the automatic fast path is skipped. - `pretty-format-json` is currently available only via `repo: builtin` while parity coverage against upstream Python behavior is still being expanded. - Other hooks from the repository which have no fast path implementation will run via the standard method. ### Disabling the fast path If you need to compare with the original behavior or encounter differences: ```bash PREK_NO_FAST_PATH=1 prek run ``` This forces prek to fall back to the standard execution path. ## 2. Explicit Builtin Repository You can explicitly tell `prek` to use its internal hooks by setting `repo: builtin`. This mode has significant benefits: - **No network required**: Does not clone any repository. - **No environment setup**: Does not create Python environments or install dependencies. - **Maximum speed**: Instant startup and execution. **Note**: Configurations using `repo: builtin` are **not compatible** with the standard `pre-commit` tool. ```yaml repos: - repo: builtin hooks: - id: trailing-whitespace - id: check-added-large-files ``` ### Supported Hooks For `repo: builtin`, the following hooks are supported: - [`trailing-whitespace`](#trailing-whitespace) (Trims trailing whitespace.) - [`check-added-large-files`](#check-added-large-files) (Prevents giant files from being committed.) - [`check-case-conflict`](#check-case-conflict) (Checks for files that would conflict in case-insensitive filesystems.) - [`check-illegal-windows-names`](#check-illegal-windows-names) (Checks for filenames which cannot be created on Windows.) - [`end-of-file-fixer`](#end-of-file-fixer) (Ensures that a file is either empty, or ends with one newline.) - [`file-contents-sorter`](#file-contents-sorter) (Sorts the lines in specified files (defaults to alphabetical).) - [`requirements-txt-fixer`](#requirements-txt-fixer) (Sorts entries in requirements.txt.) - [`fix-byte-order-marker`](#fix-byte-order-marker) (Removes UTF-8 byte order marker.) - [`forbid-new-submodules`](#forbid-new-submodules) (Prevents the addition of new Git submodules.) - [`check-json`](#check-json) (Checks JSON files for parseable syntax.) - [`check-json5`](#check-json5) (Checks JSON5 files for parseable syntax.) - [`pretty-format-json`](#pretty-format-json) (Checks that JSON files are pretty-formatted.) - [`check-toml`](#check-toml) (Checks TOML files for parseable syntax.) - [`check-vcs-permalinks`](#check-vcs-permalinks) (Ensures that links to VCS websites are permalinks.) - [`check-yaml`](#check-yaml) (Checks YAML files for parseable syntax.) - [`check-xml`](#check-xml) (Checks XML files for parseable syntax.) - [`deny-filename-pattern`](#deny-filename-pattern) (Fails if any selected filename matches a regular expression.) - [`deny-pattern`](#deny-pattern) (Fails if any file contains a matching regular expression.) - [`require-filename-pattern`](#require-filename-pattern) (Fails if any selected filename does not match a regular expression.) - [`require-pattern`](#require-pattern) (Fails if any file does not contain a matching regular expression.) - [`mixed-line-ending`](#mixed-line-ending) (Replaces or checks mixed line endings.) - [`check-symlinks`](#check-symlinks) (Checks for symlinks which do not point to anything.) - [`destroyed-symlinks`](#destroyed-symlinks) (Detects symlinks that were replaced with regular files whose contents are the original symlink target path.) - [`check-merge-conflict`](#check-merge-conflict) (Checks for files that contain merge conflict strings.) - [`detect-private-key`](#detect-private-key) (Detects the presence of private keys.) - [`no-commit-to-branch`](#no-commit-to-branch) (Protects specific branches from direct commits.) - [`check-shebang-scripts-are-executable`](#check-shebang-scripts-are-executable) (Ensures that (non-binary) files with a shebang are executable.) - [`check-executables-have-shebangs`](#check-executables-have-shebangs) (Ensures that (non-binary) executables have a shebang.) ### Hook Reference This section documents the built-in (Rust) implementations used by `repo: builtin`. #### Configuration notes - Configure arguments via `args: [...]` just like `pre-commit`. - For `repo: builtin`, `entry` is not allowed and `language` must be `system` (it is fine to omit `language`). - Some hooks are **fixers** (they modify files). Like `pre-commit-hooks`, they typically exit non-zero after making changes so you can re-run the commit. Example: ```yaml repos: - repo: builtin hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: check-added-large-files args: [--maxkb=1024] ``` --- #### `trailing-whitespace` Trims trailing whitespace from each line. **Supported arguments** (compatible with `pre-commit-hooks`): - `--markdown-linebreak-ext=` (repeatable / comma-separated) - Preserves Markdown hard line breaks (two trailing spaces) for files with the given extension(s). - Use `--markdown-linebreak-ext=*` to treat **all** files as Markdown. - `--chars=` - Trim only the specified set of characters instead of “all trailing whitespace”. - Example: `args: [--chars, " \t"]` (space + tab). **Caveats** - `--markdown-linebreak-ext` values must be extensions only (no path separators). --- #### `check-added-large-files` Prevents giant files from being committed. **Supported arguments** (compatible with `pre-commit-hooks`): - `--maxkb=` (default: `500`) - Maximum allowed file size, in kibibytes. - `--enforce-all` - Check all matched files, not just those staged for addition. **Caveats** - By default, only files staged for **addition** are checked. - Files configured with `filter=lfs` (via git attributes) are skipped. --- #### `check-case-conflict` Checks for paths that would conflict on a case-insensitive filesystem (for example macOS / Windows). **Supported arguments** - None. **Caveats** - The check includes parent directories as well as file paths, to catch directory-level case conflicts. --- #### `check-illegal-windows-names` Checks for filenames that cannot be created on Windows. **Supported arguments** - None. **Behavior / caveats** - Reports filenames containing Windows-reserved device names such as `CON`, `PRN`, `AUX`, `NUL`, `COM1`, and `LPT1`. - Reports filenames containing characters forbidden by Windows, including `<`, `>`, `:`, `"`, `\`, `|`, `?`, `*`, and control characters. - Reports path segments ending with a trailing `.` or space. --- #### `end-of-file-fixer` Ensures files end in a newline and only a newline. **Supported arguments** - None. **Behavior / caveats** - Empty files are left unchanged. - Files containing only newlines are truncated to empty. - If a file has no trailing newline, a single `\n` is appended (even if the file otherwise uses CRLF). - If a file has trailing newlines, they are reduced to exactly one trailing line ending. --- #### `file-contents-sorter` Sorts the non-empty lines in each matched file and rewrites the file when the normalized order changes. **Supported arguments** (compatible with `pre-commit-hooks`): - `--ignore-case` - Sort using ASCII case-folded ordering. - Mutually exclusive with `--unique`. - `--unique` - Sort and deduplicate lines. - Mutually exclusive with `--ignore-case`. **Behavior / caveats** - Blank lines and whitespace-only lines are removed before sorting. - Line endings are normalized to `\n` in the rewritten file. - Like upstream, the builtin hook defaults to `files: '^$'`, so you must configure `files:` explicitly to target specific files. Example: ```yaml repos: - repo: builtin hooks: - id: file-contents-sorter files: ^requirements(-dev)?\.txt$ ``` --- #### `requirements-txt-fixer` Sorts entries in Python `requirements*.txt` and `constraints*.txt` files by their case-insensitive requirement name. **Behavior / caveats** - The default file pattern is `(requirements|constraints).*\.txt$`. - Leading comments and continuation lines stay attached to their requirement while sorting. Top-of-file and trailing comment blocks are preserved. - Exact duplicate entries are collapsed, preferring the copy with an attached comment. - Exact `pkg-resources==0.0.0` and `pkg_resources==0.0.0` entries are removed, matching upstream. - This is a sorter, not a full PEP 508 validator. It uses the same lightweight name extraction as `pre-commit-hooks`. --- #### `fix-byte-order-marker` Removes a UTF-8 byte order marker (BOM) from the beginning of a file. **Supported arguments** - None. **Caveats** - Only removes the UTF-8 BOM (`EF BB BF`). --- #### `forbid-new-submodules` Prevents the addition of new Git submodules. **Supported arguments** - None. **Behavior / caveats** - Existing submodules are allowed; only submodules newly added by the checked changes are reported. - Staged changes are checked by default. When `PRE_COMMIT_FROM_REF` and `PRE_COMMIT_TO_REF` are both set, their revision range is checked instead. --- #### `check-json` Attempts to load all JSON files to verify syntax. **Supported arguments** - None. **Caveats / differences** - This implementation rejects **duplicate object keys** (errors with `duplicate key ...`). - The parser disables the default recursion limit and uses a stack-friendly drop strategy for deeply nested JSON. --- #### `check-json5` Attempts to load all JSON5 files to verify syntax. **Supported arguments** - None. **Caveats / differences** - This implementation rejects **duplicate object keys** (errors with `duplicate key ...`). --- #### `pretty-format-json` Checks that JSON files are pretty-formatted and can optionally rewrite them in place. **Supported arguments** (compatible with `pre-commit-hooks`): - `--autofix` - Rewrite files in place when formatting changes are needed. - `--indent=` (default: `2`) - Use `` for each indentation level. - Numeric values mean that many spaces. - Non-numeric values are used literally, so `--indent=\t` uses tabs. - `--no-ensure-ascii` - Keep non-ASCII characters as UTF-8 instead of escaping them as `\uXXXX`. - `--no-sort-keys` - Preserve the original key order instead of sorting object keys. - `--top-keys=` - In every JSON object, move matching keys to the front in the given order. - Duplicate names after the first one are ignored. - Remaining keys come after that prefix and are sorted unless `--no-sort-keys` is set. - This applies recursively to nested objects too, not just the root object. **Caveats** - This hook is currently available only via `repo: builtin`; automatic fast-path replacement of the upstream Python hook remains disabled until parity coverage is broader. - Rewritten files always use LF (`\n`) line endings and end with exactly one trailing newline. --- #### `check-toml` Attempts to load all TOML files to verify syntax. **Supported arguments** - None. **Caveats** - Files must be valid UTF-8; invalid UTF-8 is reported as an error. - May report multiple parse errors for a single file. --- #### `check-vcs-permalinks` Ensures that links to VCS websites are permalinks. **Supported arguments** (compatible with `pre-commit-hooks`): - `--additional-github-domain=` (repeatable) - Adds extra GitHub-style domains to check in addition to the default `github.com`. **Behavior / caveats** - Flags links of the form `https://///blob//...#L...`. - Does not flag commit-hash permalinks where `` is already a 4-64 character hexadecimal revision. - The builtin and fast-path implementations currently follow the upstream hook's GitHub-family matching behavior. --- #### `check-yaml` Attempts to load all YAML files to verify syntax. **Supported arguments** (partially compatible with `pre-commit-hooks`): - `-m`, `--allow-multiple-documents` (alias: `--multi`) - Allow YAML multi-document syntax (`---`). **Caveats / differences** - `--unsafe` is not supported. - With `repo: builtin`, passing `--unsafe` is treated as an unknown argument. --- #### `check-xml` Attempts to load all XML files to verify syntax. **Supported arguments** - None. **Caveats** - Empty files are treated as invalid XML. - Fails if there is “junk after the document element” (multiple top-level roots). --- #### `deny-filename-pattern` Fails when the final path component (the basename) of any selected file matches a configured regular expression. Patterns use the [Rust `regex` syntax](https://docs.rs/regex/latest/regex/#syntax). When multiple patterns are provided, the hook fails when a basename matches any one of them. The standard `files`, `exclude`, and type filters select which project-relative paths are checked. The patterns passed to this hook are then matched only against each selected basename. **Supported arguments** - `PATTERN...` (required) - Positional regular expressions to deny. - Use `--` before a pattern that begins with `-`. - `-i`, `--ignore-case` - Match all patterns case-insensitively. Each matching file is reported once as `path: filename matches a denied pattern`. ```yaml repos: - repo: builtin hooks: - id: deny-filename-pattern name: disallow spaces in filenames args: ['\s'] ``` --- #### `deny-pattern` Fails when any selected text file matches a configured regular expression. Patterns use the [Rust `regex` syntax](https://docs.rs/regex/latest/regex/#syntax). When multiple patterns are provided, matching any one of them is sufficient. **Supported arguments** - `PATTERN...` (required) - Positional regular expressions to deny. - Use `--` before a pattern that begins with `-`. - `-i`, `--ignore-case` - Match all patterns case-insensitively. - `-m`, `--multiline` - Search each file as a whole, with `^` and `$` matching line boundaries and `.` matching newlines. - Reads each selected file into memory. By default, each matching line is reported as `path:line:contents`. A line matching more than one pattern is reported only once. With `--multiline`, the earliest match in each file is reported as `path:start-line:matched-block`. ```yaml repos: - repo: builtin hooks: - id: deny-pattern name: disallow wildcard imports args: ['^\s*#import\s+.+:\s*\*'] files: \.typ$ ``` --- #### `require-filename-pattern` Fails when the final path component (the basename) of any selected file does not match at least one configured regular expression. This is a per-file requirement: every selected basename must match, while different basenames may match different patterns. `require-filename-pattern` supports the same positional `PATTERN...` and `-i` / `--ignore-case` arguments as [`deny-filename-pattern`](#deny-filename-pattern). Matching uses search semantics; use `^` and `$` when the pattern must match the entire basename. Files without a match are reported as `path: filename does not match any required pattern`. ```yaml repos: - repo: builtin hooks: - id: require-filename-pattern name: python tests naming args: - '^test_.*\.py$' - '^__init__\.py$' - '^conftest\.py$' files: '(^|/)tests/.+\.py$' ``` --- #### `require-pattern` Fails when any selected text file does not match at least one configured regular expression. This is a per-file requirement: every file must match, while different files may match different patterns. `require-pattern` supports the same positional `PATTERN...`, `-i` / `--ignore-case`, and `--multiline` arguments as [`deny-pattern`](#deny-pattern). Files without a match are reported as `path: no pattern matched`. ```yaml repos: - repo: builtin hooks: - id: require-pattern name: require a copyright notice args: [--ignore-case, copyright] files: '\.(rs|py)$' ``` --- #### `mixed-line-ending` Replaces or checks mixed line endings. **Supported arguments** (compatible with `pre-commit-hooks`, plus one extra mode): - `--fix=` (default: `auto`) - `auto`: replace with the most frequent line ending in the file. - `no`: check only (do not modify files). - `lf`: convert to LF (`\n`). - `crlf`: convert to CRLF (`\r\n`). - `cr`: convert to CR (`\r`) (extra mode in `prek`). **Caveats** - Empty and binary files (containing NUL) are skipped. - Upstream note: forcing `lf` / `crlf` may not behave as expected with git CRLF conversion settings (for example `core.autocrlf`). --- #### `check-symlinks` Checks for symlinks which do not point to anything. **Supported arguments** - None. **Caveats** - Relies on filesystem symlink support. On Windows, symlink creation and detection can be permission-dependent. --- #### `destroyed-symlinks` Detects files staged as regular files whose `HEAD` version is a symlink, which usually happens when a repository is checked out in an environment without symlink support. **Supported arguments** - None. **Caveats** - This matches upstream `pre-commit-hooks` behavior: it only checks tracked entries reported by `git status --porcelain=v2`. - It intentionally ignores differences consisting only of trailing ASCII whitespace (including spaces, tabs, and newline/CRLF conversions) when comparing the staged file against the original symlink target path, because those differences are commonly introduced by formatting hooks. --- #### `check-merge-conflict` Checks for merge conflict markers. **Supported arguments** (compatible with `pre-commit-hooks`): - `--assume-in-merge` - Allow running the hook even when there is no merge/rebase state detected. **Caveats** - By default, this hook exits successfully when not in a merge/rebase state. - Detects conflict markers only when they appear at the start of a line. - Detects standard conflict blocks (`<<<<<<<`, `=======`, `>>>>>>>`) and diff3 ancestor markers (`|||||||`). - `=======` is only reported after a preceding `<<<<<<<`, which avoids false positives for content such as reStructuredText headings. --- #### `detect-private-key` Detects the presence of private keys. **Supported arguments** - None. **Caveats** - This is a heuristic substring scan for common PEM/key headers (e.g. `BEGIN RSA PRIVATE KEY`, `BEGIN OPENSSH PRIVATE KEY`, `BEGIN PGP PRIVATE KEY BLOCK`, etc.). It can produce false positives/negatives. --- #### `no-commit-to-branch` Protects specific branches from direct commits. **Supported arguments** (compatible with `pre-commit-hooks`): - `-b`, `--branch ` (repeatable, default: `main`, `master`) - `-p`, `--pattern ` (repeatable) **Caveats** - This hook is configured as `always_run: true` by default, and does not take filenames. As a result, `files`, `exclude`, `types`, etc. are ignored unless you explicitly set `always_run: false`. - If HEAD is detached (no current branch), the hook does nothing. --- #### `check-executables-have-shebangs` Checks that non-binary executables have a proper shebang. **Supported arguments** - None. **Caveats** - The check is intentionally lightweight: it only verifies that the file starts with `#!`. - On systems where the executable bit is not tracked by the filesystem, `prek` consults git’s staged mode bits. --- #### `check-shebang-scripts-are-executable` Checks that non-binary files with a shebang are marked executable. **Supported arguments** - None. **Caveats** - The check is intentionally lightweight: it only verifies that the file starts with `#!`. - To work on filesystems which do not track the executable bit, `prek` consults git’s staged mode bits. --- ## File: docs/configuration.md # Configuration `prek` reads **one configuration file per project**. You only need to choose **one** format: - **prek.toml** (TOML) — recommended for new users - **.pre-commit-config.yaml** (YAML) — best if you already use pre-commit or rely on tool/editor support Both formats are first-class and will be supported long-term. They describe the **same** configuration model: you list repositories under `repos`, then enable and configure hooks from those repositories. === "prek.toml" ```toml [[repos]] repo = "https://github.com/pre-commit/pre-commit-hooks" rev = "v6.0.0" hooks = [ { id = "trailing-whitespace" }, { id = "check-toml" }, ] [[repos]] repo = "local" hooks = [ { id = "cargo-fmt", name = "cargo fmt", language = "system", entry = "cargo fmt --", types = ["rust"], pass_filenames = false, }, ] ``` === ".pre-commit-config.yaml" ```yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-toml - repo: local hooks: - id: cargo-fmt name: cargo fmt language: system entry: cargo fmt -- types: [rust] pass_filenames: false ``` ## Global configuration `prek` also reads an optional user-level global config from the platform config directory: - Linux and macOS: `~/.config/prek/prek.toml` (or `$XDG_CONFIG_HOME/prek/prek.toml` when `XDG_CONFIG_HOME` is set) - Windows: `%APPDATA%\prek\prek.toml` This file is for user-level `prek` settings, not hook definitions. Project hooks still live in the project config files described below. For the supported global settings, see the [configuration reference](reference/configuration.md#global-config-file). ## Pre-commit compatibility `prek` is **fully compatible** with [`pre-commit`](https://pre-commit.com/) YAML configs, so your existing `.pre-commit-config.yaml` files work unchanged. If you use **`prek.toml`**, there’s nothing to worry about from a `pre-commit` perspective: upstream `pre-commit` does not read TOML. If you use the same `.pre-commit-config.yaml` with both tools, avoid `prek`-only extensions or keep separate configs. Upstream `pre-commit` may warn about unknown keys or error out on unsupported features. For broader behavior differences, see [Compatibility](compatibility.md) and [Differences](diff.md). ### Prek-only extensions These entries are implemented by `prek` and are not part of the documented upstream `pre-commit` configuration surface. They work in both YAML and TOML, but they only matter for compatibility if you share a YAML config with upstream `pre-commit`. - Top-level: - [`update`](reference/configuration.md#update) - [`default_env`](reference/configuration.md#default_env) - [`priorities`](reference/configuration.md#priorities) - [`minimum_prek_version`](reference/configuration.md#prek-only-minimum-prek-version-config) - [`orphan`](reference/configuration.md#prek-only-orphan) - Repo type: - [`repo: builtin`](reference/configuration.md#prek-only-repo-builtin) - Hook-level: - [`env`](reference/configuration.md#prek-only-env) - [`shell`](reference/configuration.md#shell) - [`priority`](reference/configuration.md#prek-only-priority) - [`minimum_prek_version`](reference/configuration.md#prek-only-minimum-prek-version-hook) ## Configuration file ### Location (discovery) By default, `prek` looks for a configuration file starting from your current working directory and moving upward. It stops when it finds a config file, or when it hits the git repository boundary. If you run **without** `--config`, `prek` then enables **workspace mode**: - The first config found while traversing upward becomes the workspace root. - From that root, `prek` searches for additional config files in subdirectories (nested projects). Workspace discovery respects `.gitignore`, and also supports `.prekignore` for excluding directories from discovery. For the full behavior and examples, see [Workspace Mode](workspace.md). !!! tip After updating `.prekignore`, run with `--refresh` to force a fresh project discovery so the changes are picked up. If you pass `--config` / `-c`, workspace discovery is disabled and only that single config file is used. ### File name `prek` recognizes the following configuration filenames: - `prek.toml` (TOML) - `.pre-commit-config.yaml` (YAML, preferred for pre-commit compatibility) - `.pre-commit-config.yml` (YAML, alternate) In workspace mode, each project uses one of these filenames in its own directory. !!! note "One format per repo" We recommend using a **single format** across the whole repository to avoid confusion. If multiple configuration files exist in the same directory, `prek` uses only one and ignores the rest. The precedence order is: 1. `prek.toml` 2. `.pre-commit-config.yaml` 3. `.pre-commit-config.yml` ### File format Both `prek.toml` and `.pre-commit-config.yaml` map to the same configuration model (repositories under `repos`, then `hooks` under each repo). This section focuses on format-specific authoring notes and examples. #### TOML (`prek.toml`) Practical notes: - Structure is explicit and less indentation-sensitive. - Inline tables are common for hooks (e.g. `{ id = "ruff" }`). TOML supports both **inline tables** and **array-of-tables**, so you can choose between a compact or expanded hook style. Inline tables (best for small/simple hook configs): ```toml [[repos]] repo = "https://github.com/pre-commit/pre-commit-hooks" rev = "v6.0.0" hooks = [ { id = "end-of-file-fixer", args = ["--fix"] }, ] ``` Array-of-tables (more readable for larger hook configs): ```toml [[repos]] repo = "https://github.com/pre-commit/pre-commit-hooks" rev = "v6.0.0" [[repos.hooks]] id = "trailing-whitespace" [[repos.hooks]] id = "check-json" ``` Example: === "prek.toml" ```toml default_language_version.python = "3.12" [[repos]] repo = "local" hooks = [ { id = "ruff", name = "ruff", language = "system", entry = "python3 -m ruff check", files = "\\.py$", }, ] ``` The previous example uses multiline inline tables, a feature that was introduced in [TOML 1.1](https://toml.io/en/v1.1.0), not all parsers have support for it yet. You may want to use the longer form if your editor/IDE complains about it. === "prek.toml" ```toml default_language_version.python = "3.12" [[repos]] repo = "local" [[repos.hooks]] id = "ruff" name = "ruff" language = "system" entry = "python3 -m ruff check" files = "\\.py$" ``` #### YAML (`.pre-commit-config.yaml` / `.yml`) Practical notes: - Regular expressions are provided as YAML strings. If your regex contains backslashes, quote it (e.g. `files: '\\.rs$'`). - YAML anchors/aliases and merge keys are supported, so you can de-duplicate repeated blocks. Example: === ".pre-commit-config.yaml" ```yaml default_language_version: python: "3.12" repos: - repo: local hooks: - id: ruff name: ruff language: system entry: python3 -m ruff check files: "\\.py$" ``` #### Choosing a format **`prek.toml`** - Clearer structure and less error-prone syntax. - Recommended for new users or new projects. **`.pre-commit-config.yaml`** - Long-established in the ecosystem with broad tool/editor support. - Fully compatible with upstream `pre-commit`. **Recommendation** - If you already use `.pre-commit-config.yaml`, keep it. - If you want a cleaner, more robust authoring experience, prefer `prek.toml`. !!! tip If you want to switch, you can use [`prek util yaml-to-toml`](reference/cli.md#prek-util-yaml-to-toml) to convert YAML configs to `prek.toml`. YAML comments are not preserved during conversion. ### Scope (per-project) Each configuration file (`prek.toml`, `.pre-commit-config.yaml`, or `.pre-commit-config.yml`) is scoped to the **project directory it lives in**. In workspace mode, `prek` treats every discovered configuration file as a **distinct project**: - A project’s config only controls hook selection and filtering (for example `files` / `exclude`) for that project. - A project may contain nested subprojects (subdirectories with their own config). Those subprojects run using *their own* configs. Practical implication: filters in the parent project do not “turn off” a subproject. Example layout (monorepo with a nested project): - `foo/.pre-commit-config.yaml` (project `foo`) - `foo/bar/.pre-commit-config.yaml` (project `foo/bar`, nested subproject) If project `foo` config contains an `exclude` that matches `bar/**`, then hooks for project `foo` will not run on files under `foo/bar`: === "prek.toml" ```toml # foo/prek.toml exclude = { glob = "bar/**" } ``` === ".pre-commit-config.yaml" ```yaml # foo/.pre-commit-config.yaml exclude: glob: "bar/**" ``` But if `foo/bar` is itself a project (has its own config), files under `foo/bar` are still eligible for hooks when running **in the context of project `foo/bar`**. !!! note "Excluding a nested project" If `foo/bar/.pre-commit-config.yaml` exists but you *don’t* want it to be recognized as a project in workspace mode, exclude it from discovery using [`.prekignore`](workspace.md#discovery). Like `.gitignore`, `.prekignore` files can be placed anywhere in the workspace and apply to their directory and all subdirectories. !!! tip After updating `.prekignore`, run with `--refresh` to force a fresh project discovery so the changes are picked up. ### Validation Use [`prek validate-config`](reference/cli.md#prek-validate-config) to validate one or more config files. If you want IDE completion / validation, prek publishes a JSON Schema through the [JSON Schema Store](https://www.schemastore.org/prek.json), so some editors may pick it up automatically. That schema tracks what `prek` accepts today, but `prek` also intentionally tolerates unknown keys for forward compatibility. For every accepted configuration key and hook option, see the [Configuration Reference](reference/configuration.md). For process environment controls, see the [Environment Variable Reference](reference/environment-variables.md). --- ## File: docs/debugging.md # Debugging To enable verbose tracing output, use the `-vvv` flag when running prek: ```bash prek run -vvv ``` Additionally, on every run prek writes a log file to `~/.cache/prek/prek.log` by default. If you encounter issues, please include this log file when reporting bugs. --- ## File: docs/diff.md # Differences from pre-commit ## General differences - `prek` supports `.pre-commit-config.yaml`, `.pre-commit-config.yml`, and native `prek.toml` configuration files. Use [`prek util yaml-to-toml`](reference/cli.md#prek-util-yaml-to-toml) to convert an existing YAML config. - `prek` implements some common hooks from `pre-commit-hooks` in Rust for better performance. - `prek` supports `repo: builtin` for offline, zero-setup hooks. - `prek` uses `~/.cache/prek` as the default cache directory for repos, environments and toolchains. - `prek` decouples hook environments from their repositories, allowing shared toolchains and environments across hooks. - `prek` supports `language_version` as a semver specifier and automatically installs the required toolchains. - `prek` supports `files` and `exclude` as glob lists (in addition to regex) via `glob` mappings. See [Configuration Reference](reference/configuration.md#top-level-files). - `prek` supports a [`shell`](reference/configuration.md#shell) hook option for explicit shell-source execution through predefined adapters such as `bash`, `sh`, and `pwsh`. Upstream `pre-commit` runs `entry` directly; shell behavior must be spelled into `entry` itself. - `prek` reports more precise configuration parsing errors, including exact source locations. ## Behavioral divergences These differences intentionally change upstream behavior instead of adding a compatible superset. - File identification gives recognized extensions precedence over loose filename-prefix matches. For example, `makefile.png` is treated as a PNG image, while upstream `identify` also gives it `makefile` and `text` tags. Exact filename matches such as `Cargo.toml` still keep their name-specific tags. ## Workspace mode `prek` supports workspace mode, allowing you to run hooks for multiple projects in a single command. Each subproject can keep its own `prek.toml` or `.pre-commit-config.yaml` file. See [Workspace Mode](./workspace.md) for more information. ## Language support See the dedicated [Language Support](languages.md) page for a complete list of supported languages, prek-specific behavior, and unsupported languages. Recent releases added support for more managed hook runtimes, including Bun, Julia, Deno, and experimental .NET support. ## Managed runtime download verification For supported managed runtime downloads, `prek` verifies the downloaded archive or installer against the checksum published by the runtime's own distribution channel before extracting or installing it. This helps catch corrupted or unexpectedly changed downloads before they are unpacked. It does not add an independent trust root: if an upstream release, mirror, or checksum file is compromised together with the archive, SHA-256 verification alone cannot detect that. `uv` downloads are not covered by this behavior yet. ## Command line interface For a compatibility-focused command mapping, see [Compatibility with pre-commit](compatibility.md). ### `prek run` - `prek run [HOOK|PROJECT]...` supports selecting or skipping multiple projects or hooks in workspace mode, instead of only accepting a single optional hook id. See [Running Specific Hooks or Projects](workspace.md#running-specific-hooks-or-projects) for details. - `prek run` can execute hooks in parallel by priority (hooks with the same [`priority`](./reference/configuration.md#priority) may run concurrently), instead of strictly serial execution. - In workspace mode, `prek run` can execute independent projects at the same directory depth concurrently, while still running child projects before their parents. - `prek` provides dynamic completion for hook ids. - `prek run --dry-run` shows which hooks would run without executing them. - `prek run --last-commit` runs hooks on files changed by the last commit. - `prek run --directory ` runs hooks on a specified directory. - `prek run --no-fail-fast` lets you override the configured `fail_fast` setting for a single run and continue after failures. ### `prek install` - `prek install` and `prek uninstall` honor repo-local and worktree-local `core.hooksPath` when choosing where to manage Git shims. ### `prek validate-config` - `prek validate-config` accepts both `prek.toml` and `.pre-commit-config.yaml`. ### `prek list` `prek list` lists all available hooks, their ids, and descriptions. This provides a better overview of the configured hooks. ### `prek update` - `prek update` updates all projects in the workspace to their latest revisions. - `prek update` checks updates for the same repository only once, speeding up the process in workspace mode. - `prek update` supports `--dry-run` to preview the updates without applying them. - `prek update` supports `--exit-code` to exit non-zero when updates are available, and `--check` as an alias for `--dry-run --exit-code`. - `prek update` validates pinned SHA revisions against fetched upstream refs, including impostor-commit detection, and keeps stale `# frozen:` comments in sync when it can. - `prek update` supports the `--cooldown-days` option to skip releases newer than the specified number of days (based on the tag creation timestamp for annotated tags, or the tagged commit timestamp for lightweight tags). - `prek update` supports `--exclude-repo` to skip selected repositories while updating everything else. - `prek update` supports tag filtering with `--include-tag`, `--exclude-tag`, `--repo-include-tag`, and `--repo-exclude-tag`, using glob patterns to keep or remove matching tags before selecting an update. ### `prek sample-config` - `prek sample-config` can generate either YAML or TOML and can write directly to a file with `--file`. ### `prek util` - `prek util identify` shows the file-identification tags prek uses for filtering and debugging hook selection. - `prek util list-builtins` lists all built-in hooks bundled with prek. - `prek util yaml-to-toml` converts `.pre-commit-config.yaml` to `prek.toml`. ### `prek cache` - `prek` groups cache maintenance under `prek cache` instead of separate top-level `clean` and `gc` commands. - `prek cache gc` removes unused cached repositories, environments and toolchains, and supports `--dry-run`. - `prek cache clean` removes all cached data. - `prek cache dir` and `prek cache size` help inspect the cache before or after cleanup. ## Not implemented The `pre-commit hazmat` subcommand introduced in pre-commit [v4.5.0](https://github.com/pre-commit/pre-commit/releases/tag/v4.5.0) is not implemented. This command is niche and unlikely to be widely used. --- ## File: docs/faq.md # FAQ ## How is `prek` pronounced? Like "wreck", but with a "p" sound instead of the "w" at the beginning. The name comes from saying "pre-commit" and stopping right after the hard "k" sound; it can also be read as short for "pre-check". ## I updated `.prekignore`, why didn't discovery change? Workspace discovery is cached. If you edited `.prekignore`, run the command with `--refresh` to force a fresh project discovery so the changes are picked up. For example: ```bash prek run --refresh ``` ## What does `prek install --prepare-hooks` do? In short, it installs the Git shims **and** prepares the environments for the hooks managed by prek. It is inherited from the original Python-based `pre-commit` tool (I'll abbreviate it as **ppc** in this document) to maintain compatibility with existing workflows. It's a little confusing because it refers to two different kinds of hooks: 1. **Git shims** – Scripts placed in Git's effective hooks directory, usually `.git/hooks/` unless `core.hooksPath` points elsewhere. Both prek and ppc drop a small shim here so Git automatically runs them on `git commit`. 2. **prek-managed hooks** – The tools listed in `.pre-commit-config.yaml`. When prek runs, it executes these hooks and prepares whatever runtime they need (for example, creating a Python virtual environment and installing the hook's dependencies before execution). Running `prek install` installs the first type: it writes the Git shim so that Git knows to call prek. Which Git shims get installed is determined by `--hook-type` or `default_install_hook_types` in the config file, and defaults to `pre-commit` if neither is set. This is not affected by a hook's `stages` field in the config: `stages` controls when a configured hook may run, not which Git shims `prek install` writes. Adding `--prepare-hooks` tells prek to do that **and** proactively create the environments and caches required by the hooks that prek manages. That way, the next time Git invokes prek through the shim, the managed hooks are ready to run without additional setup. The older `--install-hooks` spelling remains as an alias. ## How does `prek install` interact with `core.hooksPath` and worktrees? If `core.hooksPath` is set in repo-local (`git config --local`) or worktree-local (`git config --worktree`) config, `prek install` and `prek uninstall` will honor it and operate on Git's effective hooks directory. If `core.hooksPath` is only configured globally or system-wide, prek refuses to install or uninstall by default. That setting may be shared across repositories, so prek avoids mutating a hook location it does not own. Use `prek install --force` to install into the repository's default hooks directory anyway. Use `--git-dir ` instead when you need to choose an explicit installation target. ## How do I use hooks from private repositories? prek supports cloning hooks from private repositories that require authentication. prek first clones with interactive terminal prompts disabled so non-interactive runs do not hang. If a clone fails with an authentication error and prek is not running in CI, it retries with terminal prompts enabled so Git can ask for credentials. In CI, interactive prompts remain disabled, so you still need to configure credentials via credential helpers, environment variables, or SSH. ### Option 1: Credential helpers (recommended) If you use GitHub CLI, Git Credential Manager, macOS Keychain, or similar tools, authentication often works automatically with no extra configuration: ```shell # GitHub CLI users: configure git to use gh for credentials gh auth setup-git # Now HTTPS URLs work automatically prek install ``` Other credential helpers that work out of the box: - **macOS**: Keychain (`credential.helper=osxkeychain`) - **Windows**: Git Credential Manager (`credential.helper=manager`) - **Linux**: GNOME Keyring, KWallet, or `credential.helper=store` You can also use `GIT_ASKPASS` to point to a custom credential program: ```shell export GIT_ASKPASS=/path/to/credential-script ``` ### Option 2: SSH URLs Use SSH URLs in your `.pre-commit-config.yaml` instead of HTTPS: ```yaml repos: - repo: git@github.com:myorg/private-hooks.git rev: v1.0.0 hooks: - id: my-hook ``` This works automatically if you have SSH keys configured with an agent. ### Option 3: URL rewriting with tokens (for CI) In CI environments without credential helpers, use environment variables to rewrite HTTPS URLs to include credentials: ```shell # GitHub Actions example export GIT_CONFIG_COUNT=1 export GIT_CONFIG_KEY_0="url.https://oauth2:${GITHUB_TOKEN}@github.com/.insteadOf" export GIT_CONFIG_VALUE_0="https://github.com/" # Or using GIT_CONFIG_PARAMETERS (more compact) export GIT_CONFIG_PARAMETERS="'url.https://oauth2:${GITHUB_TOKEN}@github.com/.insteadOf=https://github.com/'" ``` > **Security note:** Be careful with tokens in environment variables. Ensure your > CI system masks secrets in logs.