Skip to content

ci: the README claims pre-commit hooks apply, and nothing installed them - #146

Merged
ChelseaKR merged 1 commit into
mainfrom
ci/install-the-hooks-the-readme-claims
Sep 6, 2026
Merged

ci: the README claims pre-commit hooks apply, and nothing installed them#146
ChelseaKR merged 1 commit into
mainfrom
ci/install-the-hooks-the-readme-claims

Conversation

@ChelseaKR

Copy link
Copy Markdown
Owner

What was wrong

README.md's Standards Conformance table lists "pre-commit hooks" under Code Quality as a standard that applies. Measured on a fresh checkout of origin/main:

  • there is no make hooks target, and no installer script of any name;
  • CONTRIBUTING.md documents make install / make verify / make demo and never mentions hooks;
  • git grep -ni hook -- CONTRIBUTING.md Makefile docs/ returns nothing but the three hooks: keys inside .pre-commit-config.yaml;
  • .git/hooks/ holds the fourteen stock *.sample files and nothing else, and core.hooksPath is unset.

So the claim rested entirely on each contributor happening to know the command.

The mypy hook makes that worse than a missing line in a doc. .pre-commit-config.yaml declares it stages: [pre-push], and pre-commit install with no flag writes only the pre-commit hook. Even a contributor who ran the command everyone knows got gitleaks, ruff and ruff-format, and no mypy — a hook that is configured, believed, and inert. That is the shape this repository is careful about everywhere else.

What changed

make hooks, running both installs:

hooks:
	uvx pre-commit install
	uvx pre-commit install --hook-type pre-push

uvx, so no dependency is added. CONTRIBUTING.md names the target in the setup block, says why there are two commands, and says plainly that CI's make verify remains the gate of record — the hooks are a faster local signal, not the thing being relied on.

tests/test_hooks_are_declared_and_installed.py keeps the two files in step. Its real job is forward-looking: a hook declared later at a stage make hooks does not install fails there, rather than sitting configured and unrun the way the mypy hook has been. An unrecognised stage name fails rather than being skipped, so a new stage gets looked at instead of assumed covered.

It reads .pre-commit-config.yaml as text, and the docstring says why rather than leaving it as an unexamined default: no YAML library is a dependency here, adding one for this would be out of proportion, and the question is not whether the file parses — pre-commit answers that on every run — but whether a stage token in it is installed. The extraction is deliberately over-broad in the fail-closed direction.

How it was verified

make verify green: ruff, ruff-format, mypy strict, 1123 tests, 95.02% overall against a 90% floor, src/exitdrill 99% against 90, scripts/ 82% against 80.

Five negative controls, each sabotaged, confirmed landed by reading the file back, watched go red, restored from a byte copy and re-confirmed green:

Sabotage Result
hooks drops the --hook-type pre-push install red — the exact bug this fixes
hooks drops the default install red
hooks removed from .PHONY red
CONTRIBUTING.md stops naming make hooks red
a new hook declared at stages: [commit-msg], which nothing installs red

The last one is the one worth keeping: it is the failure this change exists to prevent, arriving in the future rather than the past.

Scope

Documentation and local tooling only. No source, schema, receipt, workflow or gate behaviour changes, and CI ran exactly the same targets before and after.

Prepared with AI assistance; reviewed before submission.

README.md's Standards Conformance table lists "pre-commit hooks" under
Code Quality as a standard that applies. Nothing in the repository
installed them: there was no `make hooks` target, CONTRIBUTING.md never
mentioned hooks, and a fresh clone's .git/hooks holds only the stock
samples. The claim rested on each contributor happening to know.

The mypy hook makes it worse than a missing line in a doc. It is declared
`stages: [pre-push]`, and `pre-commit install` writes only the
`pre-commit` hook, so even a contributor who ran the usual command got
gitleaks, ruff and ruff-format and no mypy. A hook configured, believed,
and inert is the shape this project is careful about everywhere else.

`make hooks` runs both installs. CONTRIBUTING.md names it and says why
there are two, and says plainly that CI's `make verify` remains the gate
of record so nobody reads the hooks as the thing being relied on.

tests/test_hooks_are_declared_and_installed.py keeps the two files in
step: a hook declared later at a stage `make hooks` does not install
fails there rather than sitting unrun. It reads the config as text and
says why, and an unrecognised stage name fails rather than being
skipped.

Five negative controls, each watched go red and restored: dropping the
pre-push install, dropping the default install, removing `hooks` from
.PHONY, removing `make hooks` from CONTRIBUTING.md, and adding a hook at
a commit-msg stage nobody installs.
@ChelseaKR
ChelseaKR merged commit 857a411 into main Sep 6, 2026
12 checks passed
@ChelseaKR
ChelseaKR deleted the ci/install-the-hooks-the-readme-claims branch September 6, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant