docs/workspace.md
prek supports a powerful workspace mode that allows you to manage multiple projects with their own pre-commit configurations within a single repository. This is particularly useful for monorepos or projects with complex directory structures.
A workspace is a directory structure that contains:
.pre-commit-config.yaml file.pre-commit-config.yaml files in subdirectoriesEach directory containing a .pre-commit-config.yaml file is considered a project. Projects can be nested infinitely deep.
When you run prek run without the --config option, prek automatically discovers the workspace:
Find workspace root: Starting from the current working directory, prek walks up the directory tree until it finds a .pre-commit-config.yaml file. This becomes the workspace root.
Discover all projects: From the workspace root, prek recursively searches all subdirectories for additional .pre-commit-config.yaml files. Each one becomes a separate project.
Git repository boundary: The search stops at the git repository root (.git directory) to avoid including unrelated projects.
!!! note
**Workspace root**
- The workspace root is not necessarily the same as the git repository root, a workspace can exist within a subdirectory of a git repository.
- The current working directory determines the workspace root discovery. `prek` starts searching from your current location and stops at the first `.pre-commit-config.yaml` file found while traversing up the directory tree. Running from different directories may discover different workspace roots. Use `prek -C <dir>` to change the working directory before execution.
**Discovery exclusions**
- Directories beginning with a dot (e.g. `.hidden`) are ignored during project discovery.
- Cookiecutter template directories (names like `{{cookiecutter.project_slug}}`) are ignored during project discovery.
**Ignore rules**
- By default, `prek` respects `.gitignore` files during workspace discovery. This means any directories or files excluded by `.gitignore`, `.git/info/exclude`, or your global gitignore configuration will automatically be excluded from project discovery. This prevents `prek` from discovering workspaces in ignored directories like `node_modules`, `target`, or `.venv`.
- For additional control, `prek` also supports reading `.prekignore` files (following the same syntax rules as `.gitignore`) to exclude specific directories from workspace discovery beyond what's in `.gitignore`. Like `.gitignore`, `.prekignore` files can be placed anywhere in the workspace and apply to their directory and all subdirectories. This works similarly to the `--skip` option but is configured via files.
!!! tip
After updating `.prekignore`, run with `--refresh` to force a fresh project discovery so the changes are picked up.
my-monorepo/
├── .pre-commit-config.yaml # Workspace root config
├── .git/
├── docs/
│ └── .pre-commit-config.yaml # Nested project
├── src/
│ ├── .pre-commit-config.yaml # Nested project
│ └── backend/
│ └── .pre-commit-config.yaml # Deeply nested project
└── frontend/
└── .pre-commit-config.yaml # Nested project
In this example:
my-monorepo/ is the workspace rootdocs/, src/, src/backend/, and frontend/ are individual projects.pre-commit-config.yaml fileWhen running in workspace mode:
prek collects all files within the workspace root directoryImportant: Each project can only see and process files within its own directory tree. This is a fundamental design principle of workspace mode that ensures proper isolation between projects.
A hook defined in frontend/.pre-commit-config.yaml can only match files under the frontend/ directory—it cannot reference files from sibling directories like backend/. If hooks need to reference files across multiple projects, move the hook configuration to a common ancestor directory (e.g., the workspace root).
For each project:
Projects are executed from deepest to shallowest:
src/backend/ (deepest)src/docs/frontend/./ (root, last)This ensures that more specific configurations (deeper projects) take precedence over general ones.
By default, files in subprojects will be processed multiple times - once for each project in the hierarchy that contains them. For example, a file in src/backend/ will be checked by hooks in src/backend/, then src/, then the workspace root.
To isolate a project, you can set orphan: true in its configuration. When enabled, files in this project are "consumed" by it and will not be processed by parent projects:
# src/backend/.pre-commit-config.yaml
orphan: true
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
With this option:
src/backend/ are processed only by hooks in src/backend/src/ (but not in src/backend/) are processed by hooks in src/ and the workspace rootThis can be useful to avoid redundant processing in monorepos with nested project structures or to completely isolate a subproject from parent configurations.
When running prek run on the example structure above, you might see output like this:
$ prek run
Running hooks for `src/backend`:
check python ast.........................................................Passed
check for merge conflicts................................................Passed
black....................................................................Passed
isort....................................................................Passed
Running hooks for `docs`:
Markdownlint.........................................(unimplemented yet)Skipped
Running hooks for `frontend`:
prettier.................................................................Passed
Running hooks for `src`:
isort....................................................................Passed
mypy.....................................................................Passed
check python ast.........................................................Passed
check docstring is first.................................................Passed
Running hooks for `.`:
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
trim trailing whitespace.................................................Passed
check for merge conflicts................................................Passed
Notice how:
src/backend/ are processed by both the src/backend/ project and the src/ projectWhen you combine orphan: true with selectors such as --skip, remember that orphans keep the files they cover. Even if you skip an orphan project (for example via --skip src/backend/), that project still claims ownership of the files under its directory. Those files will not fall back to parent projects, so you can disable or precisely target orphaned projects without reintroducing duplicate processing upstream.
# Run from current directory, auto-discover workspace
prek run
# Run specific hook across all projects
prek run black
# Run from specific directory
cd src/backend && prek run
# Use -C option to change directory automatically
prek run -C src/backend
The -C <dir> or --cd <dir> option automatically changes to the specified directory before running, allowing you to target specific projects from any location in the workspace.
!!! note
When using `prek install`, only the workspace root configuration's `default_install_hook_types` will be honored. Nested project configurations are not considered during installation.
In workspace mode, you can selectively run hooks from specific projects or skip certain projects/hooks using flexible selector syntax.
The selector syntax has three different forms:
<hook-id>: Matches all hooks with the given ID across all projects.<project-path>/: Matches all hooks from the specified project and its subprojects.<project-path>:<hook-id>: Matches only the specified hook from the specified project.Selectors can be used to select specific hooks or projects, and combined with --skip to exclude certain hooks or projects.
!!! note
`<project-path>` can be a relative path, which is then resolved relative to the current working directory.
The trailing slash `/` in a `<project-path>` is important: if a selector does not contain a slash, it is interpreted as a hook ID.
!!! note "Hook ids containing :"
If your hook id contains `:` (for example `id: lint:ruff`), `prek run lint:ruff`
will not select that hook. `prek` interprets `lint:ruff` as the selector
`<project-path>:<hook-id>`, with project `lint` and hook `ruff`.
To select the hook id `lint:ruff`, add a leading `:` and run
`prek run :lint:ruff`.
# Run all hooks with a specific ID across all projects
prek run <hook-id>
# Run only hooks from a specific project
prek run <project-path>/
# Run only hooks with a specific ID from a specific project
prek run <project-path>:<hook-id>
Examples:
# Run all 'black' hooks across all projects
prek run black
# Run all hooks from the 'frontend' project
prek run frontend/
# Run only the 'lint' hook from the 'frontend' project
prek run frontend:lint
# Run the 'lint' from 'frontend' and 'black' from 'src/backend'
prek run frontend:lint src/backend:black
You can skip specific projects or hooks using the --skip option, with the same syntax as for selecting projects or hooks.
Alternative: You can also create .prekignore files (using .gitignore syntax) anywhere in the workspace to permanently exclude directories from project discovery during workspace setup. Note that .gitignore files are already respected by default, so .prekignore is only needed for excluding additional directories beyond what's in .gitignore.
!!! tip
After updating `.prekignore`, run with `--refresh` to force a fresh project discovery so the changes are picked up.
# Skip all hooks from a specific project
prek run --skip <project-path>/
# Skip specific hooks within a selected project
prek run <project-path>/ --skip <subproject-path>/
# Skip all hooks with a specific ID across all projects
prek run --skip <hook-id>
Examples:
# Run all hooks except those from the 'frontend' project
prek run --skip frontend/
# Run hooks from 'frontend' but skip 'frontend/docs'
prek run frontend/ --skip frontend/docs
# Run hooks from 'frontend' but skip 'frontend/docs' and 'frontend:lint'
prek run frontend/ --skip frontend/docs --skip frontend:lint
# Run all hooks except 'black' and 'markdownlint' hooks
prek run --skip black --skip markdownlint
!!! note
Selecting a project includes all its subprojects unless explicitly skipped. Skipping a project also skips all its subprojects.
!!! note
The `PREK_SKIP` or `SKIP` environment variable can be used as an alternative to `--skip`. Multiple values should be comma-delimited:
# Skip 'frontend' and 'tests' projects
PREK_SKIP=frontend/,tests prek run
# Skip 'frontend/docs' project and 'src/backend:lint' hook
SKIP=frontend/docs,src/backend:lint prek run
Precedence rules for --skip command line options and environment variables are: --skip > PREK_SKIP > SKIP.
# Run 'lint' hooks from all projects except 'tests'
prek run lint --skip tests
# Run all hooks from 'src' and 'docs' but skip 'src/legacy'
prek run src/ docs/ --skip src/legacy
# Run 'format' hooks only from Python projects
prek run python:format
When you specify a configuration file using the -c or --config parameter, workspace mode is disabled and only the specified configuration file is used. This mode provides traditional pre-commit behavior similar to the original pre-commit tool.
In single config mode:
# Disable workspace mode, use specific config
prek run --config .pre-commit-config.yaml
# Use config from a subdirectory
prek run --config src/.pre-commit-config.yaml
# Short form using -c
prek run -c docs/.pre-commit-config.yaml
| Feature | Workspace Mode | Single Config Mode |
|---|---|---|
| Discovery | Auto-discovers all .pre-commit-config.yaml files | Uses single specified config file |
| Working Directory | Uses workspace root | Uses git repository root |
| File Scope | All files in workspace | All files in git repo |
| Hook Scope | Project-specific file filtering | All files pass to all hooks |
| Execution Context | Each project runs in its own directory | All hooks run from git root |
| Configuration | Multiple configs | Single config file only |
To migrate an existing single-config setup to workspace mode:
.pre-commit-config.yaml to repository root.pre-commit-config.yaml in subdirectories as neededfiles/exclude patterns to be project-relativeTo improve performance in large monorepos, prek introduces a workspace cache mechanism. The workspace cache stores the results of project discovery, so repeated runs are much faster.
.pre-commit-config.yaml files, remove projects, or otherwise change the workspace structure, prek will usually detect this and refresh the cache automatically..pre-commit-config.yaml to your workspace, prek may not detect it immediately, try running with --refresh to ensure the cache is up to date.prek run --refresh
This will clear and rebuild the workspace cache before running hooks.
When running in workspace mode, there are a few changes to the output format and behavior compared to single-config mode:
The workspace mode provides powerful organization capabilities while maintaining backward compatibility with existing single-config workflows.