SuperAPI AI Directory

pre-commit

Writing & research

View repository

Updated 10-15 22:40

About this skill

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 → prek
  • pre-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`:

```yaml
# .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.