pre-commit is designed to support cross-platform, language-agnostic commit hooks.
While this is useful for some tasks that truly need to be consistent across languages
and repositories, the philosophy demonstrated here is different. It starts with the following
principle:
"Simple" is a deceptive concept, because tools can appear to be simple while simply hiding or delaying complexity until things change or go wrong. It is important to consider the entire set of environments you are maintaining, which includes local dev, git hooks, CI, and editor integration, as well as debugging errors and maintaining or adding new tasks.
I believe the default usage of pre-commit (which encodes commands in separately-maintained repositories) has some problems. Instead, I recommend a "native-first" approach that includes the following:
- Using a modern, reliable package/environment manager for the language you are working in (
uvfor python,cargofor Rust,pnpmfor Typescript, etc.) - A human-readable, minimal set of tasks that serves as the source of truth
- Consistency across local development, GitHub actions, and hooks
- Preferring the native / default configuration for each tool so that it is easy to look up in docs and debug
First, you should choose a dependency manager and add dependencies
like linters, test runners etc. natively. Where possible, use the default
suggested configuration options for those tools.
For example, for a python project, you might choose uv for dependency management
and ruff for linting/formatting. You would add ruff by running uv add ruff,
and then a minimal set of configuration to the [tool.ruff] section of pyproject.toml
Next, you should think about what environments you need to support. This is probably some or all of:
- Local development via command line (Linux, Mac, maybe Windows?)
- An IDE/editor (e.g., vscode, which can apply formatting on save)
- Hooks (via pre-commit)
- CI (GitHub via GitHub actions)
- Cloud infrastructure (e.g., deploying an app or running batch jobs)
You should then define a set of tasks (using a tool like just, Make,
or potentially an integrated script runner for your package manager)
that cover all the basic things you need to do.
You may have slightly different tasks for the environments you listed above.
An example:
# Install all dependencies
install:
uv lock --upgrade
uv sync --all-extras --frozen
# Run the application
run: install
uv run main.py
# Run unit tests
test *args: install
uv run pytest {{ args }}
# Read-only check for formatting/lint rules
check:
uv run ruff format --check
uv run ruff check --fix
# Modify files to comply with formatting/lint rules
fix: install
uv run ruff format
uv run ruff check --fix
In addition to your base tasks, define some tasks specifically to be called in your hooks. These will likely just reference other tasks:
pre-commit: install
@just check
pre-push: install
@just check
@just test
You may also want to add a setup task to help people install or update hooks. Note that if most of the details are in the tasks themselves, you will not have to update hooks very often.
setup: install hook
hook: install
uv run pre-commit install --install-hooks --overwrite
unhook: install
uv run pre-commit uninstall
Finally, you need to set up .pre-commit-config.yaml to reference your tasks:
repos:
- repo: local
hooks:
- id: pre-commit
name: Runs before every commit
entry: just
args: [pre-commit]
language: system
pass_filenames: false
- id: pre-push
name: Runs before pushing to a remote
entry: just
args: [pre-push]
language: system
pass_filenames: false
stages: [pre-push]Note that hooks can be optionally installed; it is always possible to call
commands directly from the command line (e.g. just check)
GitHub actions (for CI) should also reference tasks. For example:
name: CI
on:
push:
branches: main
pull_request:
branches: main
jobs:
check-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install just
uses: extractions/setup-just@v3
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "latest"
- name: Set up python
run: just setup-python
- name: Install dependencies
run: just install
- name: Run linting/formatting
run: just check
- name: Run tests
run: just testThe rest of this README describes how to setup and run the example in this repository.
This project can be developed on Mac, Linux, WSL, or Windows, and requires the following dependencies:
justfor task runninguvfor package and environment management
We use uv for python versioning.
Install just (or check the official docs)
| OS | Command |
|---|---|
| Mac | brew install just |
| Debian/Ubuntu | apt install just |
| Arch | pacman -S just |
Install uv (or check the official docs):
curl -LsSf https://astral.sh/uv/install.sh \| shInstall uv:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 \| iex"`If you have a shell installed (Git for Windows, GitHub Desktop, or Cygwin), you can install
and use just:
[TODO does this work properly?]
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DESTIf you don't, you can call uv commands directly from powershell
(for example, install of calling just test you can run uv run pytest). Take a look at the
justfile to see what the corresponding commands are.
To set up git pre-commit/pre-push hooks, a compatible version of python
and dependencies for this project, run the following command:
just setupIf you wish to skip setting up hooks, you can skip this.
You can now call just --list to see all available commands.
