Git Hooks (prek)
prek is a Rust rewrite of pre-commit: same .pre-commit-config.yaml, same .pre-commit-hooks.yaml, no Python runtime required. Default to prek for new setups. A repo already on pre-commit keeps working — most commands below carry over unchanged, but not all; see "prek vs pre-commit CLI" before assuming a 1:1 mapping.
Detect the installed tool
Read line 2 of .git/hooks/pre-commit:
github.com/j178/prek→ prekpre-commit.com→ pre-commit
Install
uv tool install prek # or: pip install prek / cargo install prek
prek --version
prek install # pre-commit stage only
prek install --hook-type prepare-commit-msg # add prepare-commit-msg
prek install -t pre-commit -t prepare-commit-msg # multiple stages
prek install --prepare-hooks # also build hook envs now
prek install --overwrite # replace an existing shim
Install more stages by default without repeating --hook-type:
# .pre-commit-config.yaml
default_install_hook_types: [pre-commit, prepare-commit-msg]
Hook stages
Stage names match git hook names directly:
| Stage | Fires | Use for |
|---|---|---|
pre-commit |
Before commit creation | Formatting, linting, tests |
prepare-commit-msg |
Before the message editor | Rewriting the commit message |
commit-msg |
After the message is written | Validating the commit message |
pre-push |
Before push to remote | Integration tests, security scans |
pre-merge-commit |
Before a merge commit | Merge validation |
post-checkout |
After checkout | Environment setup |
post-commit |
After commit created | Notifications, logging |
post-merge |
After merge completes | Dependency updates |
manual |
Explicit invocation only | On-demand tasks |
prepare-commit-msg vs commit-msg
| prepare-commit-msg | commit-msg | |
|---|---|---|
| Can modify the message | Yes | No — validation only |
| Runs | Before the editor opens | After the message is written |
| Env vars | PRE_COMMIT_COMMIT_MSG_SOURCE, PRE_COMMIT_COMMIT_OBJECT_NAME |
none |
| Use for | Rewriting, formatting | Validation, rejection |
For message validation, load Skill(skill: "commitlint:commitlint") or Skill(skill: "conventional-commits:conventional-commits") instead of building a hook.
Configuration files
.pre-commit-config.yaml — user repository
| Property | Type | Default | Purpose |
|---|---|---|---|
repos |
list | required | Repository mappings |
default_install_hook_types |
list | [pre-commit] |
Hook types installed by default |
default_stages |
list | all stages | Default stages for hooks |
fail_fast |
bool | false |
Stop on first hook failure |
repos:
- repo: https://github.com/org/tool
rev: v1.0.0 # immutable ref — tag or SHA, never a branch name
hooks:
- id: hook-name
stages: [prepare-commit-msg]
args: [--option, value]
Hook-level properties: id (required), stages, args, files/exclude (regex), types (AND logic), always_run, pass_filenames, verbose, require_serial.
.pre-commit-hooks.yaml — hook repository
Defines hooks for distribution. Required: id, name, entry, language. Optional: stages, pass_filenames (default true), always_run (default false), files/exclude, types, description, minimum_pre_commit_version.
- id: commit-polish
name: Polish Commit Message
description: Rewrites commit messages to conventional format using LLM
entry: commit-polish
language: python
stages: [prepare-commit-msg]
pass_filenames: false # hook receives the message file path, not staged files
always_run: true # run even without staged file changes
minimum_pre_commit_version: "3.2.0"
Implementing a prepare-commit-msg hook
Receives the commit message file path as sys.argv[1], plus PRE_COMMIT_COMMIT_MSG_SOURCE (message/template/merge/squash/commit) and PRE_COMMIT_COMMIT_OBJECT_NAME (commit SHA, for amends) as env vars.
#!/usr/bin/env python3
"""Hook entry point for prepare-commit-msg stage."""
import os
import sys
def main() -> int:
if len(sys.argv) < 2:
print("Error: No commit message file provided", file=sys.stderr)
return 1
commit_msg_file = sys.argv[1]
source = os.environ.get("PRE_COMMIT_COMMIT_MSG_SOURCE", "")
commit_sha = os.environ.get("PRE_COMMIT_COMMIT_OBJECT_NAME", "")
with open(commit_msg_file, encoding="utf-8") as f:
original_message = f.read()
if not original_message.strip():
return 0 # nothing to rewrite
new_message = process_commit_message(original_message)
with open(commit_msg_file, "w", encoding="utf-8") as f:
f.write(new_message)
return 0
def process_commit_message(message: str) -> str:
return message
if __name__ == "__main__":
sys.exit(main())
Register the entry point in pyproject.toml:
[project.scripts]
commit-polish = "commit_polish.hook:main"
# .pre-commit-hooks.yaml
- id: commit-polish
name: Polish Commit Message
entry: commit-polish
language: python
stages: [prepare-commit-msg]
pass_filenames: false # critical: hook needs the message file path
always_run: true # critical: run even without staged files
Running hooks
git commit -m "message" # auto: pre-commit + prepare-commit-msg hooks
git push # auto: pre-push hooks
prek run # staged files only — default, preferred
prek run --files path/to/file.py # scoped to specific files
prek run commit-polish # one hook
prek run --stage prepare-commit-msg # one stage
prek run commit-polish --verbose
prek run --all-files # whole repo — only when the user asks for it
--all-files formats the entire repository: it pollutes diffs with unrelated changes, creates merge conflicts on files others are editing, and breaks git blame. Scope to staged files (no args) or --files <paths> unless the user explicitly requests a repository-wide cleanup.
Testing a hook before distributing it
prek try-repo /path/to/hook-repo hook-id --all-files
prek try-repo /path/to/hook-repo hook-id --files a.py
For a message-mutating hook, skip the framework and run the entry point directly against a scratch file:
echo "test message" > /tmp/test-msg
python -m commit_polish.hook /tmp/test-msg
cat /tmp/test-msg
Skipping hooks
SKIP=hook-id git commit -m "message" # skip one hook during a real commit
prek run --skip hook-id # skip one hook for a manual run
git commit --no-verify # bypass pre-commit and commit-msg stage hooks only
--skip only applies to prek run; git's automatic invocation never sees it. Use SKIP=hook-id for that instead — prek honors it exactly like pre-commit (verified against 0.4.11: it errors out if SKIP filters out every hook in the config, so never use it to skip an entire single-hook setup).
--no-verify does not bypass prepare-commit-msg — git only wires it to the pre-commit and commit-msg stages (verified against git commit -h and by reproducing a live prepare-commit-msg hook run with the flag set), so a message-rewriting hook still fires. Some repos forbid --no-verify outright (fix the failing hook instead) — check the project's own contribution docs before reaching for it.
Cache
prek cache dir # show cache location
prek cache size # show cache size
prek cache gc # remove unused cached repos/envs
prek cache clean # remove all cached data
Override the cache directory with PREK_HOME.
prek vs pre-commit CLI
Most subcommands carry over unchanged (run, install, uninstall, list, try-repo, validate-config, validate-manifest, sample-config). These do not:
| Task | pre-commit | prek |
|---|---|---|
| Update hook revisions | pre-commit autoupdate |
prek update |
| Restrict a run to one stage | run --hook-stage X |
run --stage X |
| Clear the cache | pre-commit clean / pre-commit gc |
prek cache clean / prek cache gc |
Common patterns
# Commit message rewriting
- repo: https://github.com/your-org/commit-polish
rev: v1.0.0
hooks:
- id: commit-polish
stages: [prepare-commit-msg]
pass_filenames: false
always_run: true
# Commit message validation
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.5.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]
# Run formatting on pre-commit, integration tests on pre-push
- repo: local
hooks:
- id: python-tests
name: Run Python Tests
entry: uv run pytest
language: system
stages: [pre-commit]
types: [python]
pass_filenames: false
- id: integration-tests
name: Run Integration Tests
entry: uv run pytest tests/integration
language: system
stages: [pre-push]
pass_filenames: false
always_run: true
Common issues
Hook not running — verify the shim exists (ls -la .git/hooks/prepare-commit-msg), install the missing stage (prek install --hook-type prepare-commit-msg), and check default_install_hook_types in .pre-commit-config.yaml.
Message hook receives filenames instead of the message path — set pass_filenames: false for prepare-commit-msg and commit-msg stage hooks.
Hook skipped when no files match — set always_run: true.
Repo updates not reflected after prek update — the config used a mutable ref. Use a tag or SHA (rev: v1.0.0 or rev: a1b2c3d4), never a branch name (rev: main).
Hooks run in an unexpected order — hooks run in the order listed in .pre-commit-config.yaml within a repository entry; hooks from different repository entries may run in parallel. Group dependent hooks in one repository entry, or set require_serial: true on the later one.
Complete example: commit message workflow
commit-polish/
├── .pre-commit-hooks.yaml
├── pyproject.toml
└── src/
└── commit_polish/
├── __init__.py
└── hook.py
# .pre-commit-hooks.yaml
- id: commit-polish
name: Polish Commit Message
description: Rewrites commit messages to conventional commits format
entry: commit-polish
language: python
stages: [prepare-commit-msg]
pass_filenames: false
always_run: true
minimum_pre_commit_version: "3.2.0"
# consumer's .pre-commit-config.yaml
default_install_hook_types: [pre-commit, prepare-commit-msg]
repos:
- repo: https://github.com/your-org/commit-polish
rev: v1.0.0
hooks:
- id: commit-polish
stages: [prepare-commit-msg]
cd /path/to/consumer-repo
prek install # installs pre-commit + prepare-commit-msg shims
git add .
git commit -m "fix bug" # hook rewrites the message before the editor opens
Version requirements
| Component | Minimum | Notes |
|---|---|---|
| prek | 0.4 | CLI surface in this skill verified against 0.4.11 |
| pre-commit | 3.2.0 | Stage names match git hook names — needed for the stage table above |
| Git | 2.24+ | Required for the pre-merge-commit stage |
References
references/pre-commit-official-docs.md has the full link set. Config schema, hook stages, and .pre-commit-hooks.yaml are pre-commit's format and pre-commit.com's docs describe them accurately for prek too — only the CLI differs, per the table above.