One self-contained script. Copy it into hooks/pre-commit/<name>, edit the
header, and chmod +x it. No pip install. The
multiplexer
runs it as a subhook.
Works as a silent lint (ruff, mypy, shellcheck, rubocop) or with optional
yes/no prompts on /dev/tty. Defaults make a copied file a strict, silent
no-op (require git, then exit 0) until TOOL, run_check, or
UPFRONT_PROMPT is set.
Python 3.13 or newer on PATH. The only file you need is
src/pre-commit-template. Install the multiplexer as .git/hooks/pre-commit
if you want this file run as a subhook.
mkdir -p hooks/pre-commit
cp src/pre-commit-template hooks/pre-commit/01-ruff
chmod +x hooks/pre-commit/01-ruffEdit the header only. Leave everything below # STOP HERE alone.
REQUIRED_COMMANDS = ["ruff"]
TOOL = ["ruff", "check"]
FILE_EXTENSIONS = [".py"]With no matching staged files the hook exits 0. You can run it by hand:
hooks/pre-commit/01-ruff| Name | Default | Meaning |
|---|---|---|
REQUIRED_COMMANDS |
[] |
Extra binaries that must exist on PATH. The engine always requires git. |
TOOL |
None |
Argv prefix; matching files are appended. Empty list is unset. |
FILE_EXTENSIONS |
[] |
Match suffix (e.g. [".py", ".sh"]). Empty means no extension filter. |
SHEBANG_PATTERN |
None |
Regex tested against the first line (e.g. r"python3?"). |
RUN_MODE |
"batch" |
"batch": one invocation with all files. "each": one invocation per file. |
UPFRONT_PROMPT |
None |
Yes/no text on /dev/tty before the check. None = skip. |
UPFRONT_WHEN |
None |
If a prompt is set: None means always; otherwise call the predicate. |
CONFIRM_FINDINGS_PROMPT |
None |
Yes/no after a failed check. None = abort on failure. |
ON_MISSING_TOOL |
"abort" |
"abort" or "skip" when a name in REQUIRED_COMMANDS is missing. |
ON_NO_TTY_UPFRONT |
"abort" |
"abort" or "skip" when an upfront prompt cannot open /dev/tty. |
ON_NO_TTY_FINDINGS |
"abort" |
"abort" or "skip" when a findings prompt cannot open /dev/tty. |
match_file |
None |
(path: str) -> bool after the built-in filter; extra AND. |
run_check |
None |
(files: list[str]) -> int instead of TOOL. Print your own output; return a code. |
should_upfront_prompt |
None |
() -> bool instead of UPFRONT_WHEN. |
A staged file is a candidate if it is readable. It then matches if:
FILE_EXTENSIONSandSHEBANG_PATTERNare both unset/empty → all readable staged files; or- its name ends with an extension or its shebang matches (either filter may be set alone).
git is never skipped. Missing git always aborts with exit 1, regardless
of ON_MISSING_TOOL. Unknown RUN_MODE or ON_* values print a message on
stderr and exit 1.
When-predicate order: should_upfront_prompt if set, else UPFRONT_WHEN if
set, else always (when UPFRONT_PROMPT is set). The engine exposes
current_branch() -> str for those callables.
Prompt-only hook: TOOL is unset or empty and run_check is unset. The
engine still requires git, runs the upfront prompt if configured, and does
not list or check staged files.
Staged paths come from git diff --cached --name-only --diff-filter=ACM,
split on newlines so names with spaces stay intact. Missing or unreadable
paths are skipped; they do not abort the hook.
Read yes/no answers from /dev/tty, never from stdin. Git and the
multiplexer may already be using stdin for a payload. Accept y / yes /
n / no (case-insensitive); re-ask on anything else. If /dev/tty
cannot be opened, apply that hook’s ON_NO_TTY_* knob — do not hang on a
pipe.
| Knob | "abort" |
"skip" |
|---|---|---|
ON_NO_TTY_UPFRONT |
Message on stderr, exit 1 | Continue |
ON_NO_TTY_FINDINGS |
Message on stderr, exit 1 | Treat as pass |
Exit 0 means no matching files, the check passed, the user confirmed
findings, prompt-only “yes”, or missing-tool skip. Exit 1 means no Git
work tree, missing tool (abort), the user said no, no-TTY abort, or a
failed check with no code. Otherwise the hook exits with the tool’s
non-zero code.
Docs-only headers. This repo does not ship finished hook scripts.
Strict lint default: require ruff, check staged Python files (and
shebang matches), abort on tool failure.
REQUIRED_COMMANDS = ["ruff"]
TOOL = ["ruff", "check"]
FILE_EXTENSIONS = [".py"]
SHEBANG_PATTERN = r"python3?"Prompt only on master. When /dev/tty is unavailable (IDE / Cursor),
skip the prompt so the commit is not blocked.
UPFRONT_PROMPT = "Are you sure you want to commit to master? [Yes/No] "
UPFRONT_WHEN = lambda: current_branch() == "master"
ON_NO_TTY_UPFRONT = "skip"Leave TOOL unset for a prompt-only hook.
These steps are only for changing this repository.
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
python -m pytest