Ratchet: a code-size budget for your repo that only goes down.
A small tool that refuses any change which grows your code past its budget, and makes every raise an explicit, logged grant that shows up in the diff. How it works, how I run it on its own repo, and where it does not help.
A code-size budget for your repo that only goes down.
Coding agents are good at adding code and bad at deciding not to. Each change is locally correct, each addition is a few lines, and nothing gets deleted. One day the codebase is much bigger than anyone meant it to be, and there is no commit you can point at, because there was never one decision.
Ratchet puts a number on that and makes the number one-way. It is on PyPI as ratchet-size and on GitHub at krishansubudhi/ratchet.
The pitch can sound like “no code”, and that is not the goal. The goal is less code by adding a guard. A feature that needs six hundred lines should get six hundred lines. What I wanted was for someone to say yes out loud, with a reason written down, and for nothing else to be able to raise the number. The budget is the guard. The grant is the yes.
How it works
Three parts:
- The budget.
.ratchet.jsonholds a ceiling for each group,sourceandtests. Only code files count. Blank lines don’t, and neither do docs. - The check.
ratchet checkruns in CI, in a git hook, or inside the agent’s own session. If a group is over its ceiling it exits 1 and says what grew. The refusal text is written for an agent: it offers to cut lines or to stop and ask a human, and it tells the agent not to edit the budget file to get around it. - The grant.
ratchet grant +54 --group source --reason "..."raises the ceiling and appends a line to.ratchet-grants.jsonlwith the reason and who ran it. You commit both files together, so the raise shows up in the diff.
Shrinking does not move the ceiling by itself. ratchet tighten does that when you run it, and until you do, the room you freed is still there to spend. I come back to what that costs.
An illustration, not a measurement. Each click is a line removed. The pawl stops the wheel turning back, and a grant is the only thing that lifts it.
Quick start
I ran these in a fresh repo with the 0.2.3 release from PyPI, and each one did what it says:
1
2
3
pip install ratchet-size
ratchet init
ratchet check
Order matters. Run ratchet check before init and it exits 2 with “no .ratchet.json here; run ratchet init first”. init writes the budget at today’s size:
1
2
3
4
5
wrote .ratchet.json
source ceiling 4 lines
tests ceiling 0 lines
per-file limit 400 lines, 0 file(s) already over it recorded at their size
next: commit .ratchet.json, then run `ratchet check` before every commit
Look at that ceiling: it is exactly today’s size. init defaults to zero slack, so the first real change gets refused. Pass --slack to keep some room. How I picked mine is in the headroom post.
Then I added two lines to app.py in that repo and ran ratchet check:
1
2
3
4
5
6
7
8
9
10
ratchet: refused -- source grew 2 lines past its limit (6 / 4)
app.py +2
Fix it one of two ways:
1. remove 2 lines of code (dead code, duplicates), or
2. stop and ask a human to allow the growth (humans only -- never run this yourself):
ratchet grant +2 --group source --reason "why"
Do not edit .ratchet.json or .ratchet-grants.jsonl to get past this.
It exits 1. A human who runs the grant it suggests gets a logged raise, and the commit goes in as a one-line diff for review.
The history in this repo
Ratchet runs on itself in CI, so its own history is the best test I have. It is short: 32 commits in under a week, and two lines in the grants log. The numbers are the measured line counts at each commit, taken from a clone with ratchet’s own counter.
- Oct 6. The first commit sets the budget. Source measures 690 lines, so the ceiling is 710, which is 20 lines of slack. Tests measure 269, ceiling 289.
- Oct 7. One commit adds a budget command and two hooks. It grows source by 54 lines and tests by 33, and two grants log that growth. Each grant lands the ceiling exactly on the measured total: 764 source, 322 tests. That is the zero-headroom trap from twenty-one raises, and I would rather say so than dress it up.
- Oct 9 to 11. Releases 0.2.0 through 0.2.3. Source drops one line to 763 and the ceiling stays at 764, because nobody ran
ratchet tighten. The test budget has been full since the grant: 322 of 322.
Both grant lines have something I want to be plain about. The promise is not that a human typed every grant. It is that no raise happens silently: each one is a named command with a reason, a line in the log and a diff someone can review. On ratchet’s own repo, both raises went in exactly that way, and each one is still there to read. If you want only people to grant, the next section says what that takes.
Honest limits
- It counts lines, not quality. A dense rewrite that packs the same logic into fewer lines passes the check. The refusal text says “never compress working code to fit”, but that is an instruction to the agent, not a check on the code.
- Tests count too, so deleting one banks room. Ratchet cannot tell a dead test from a load-bearing one. That protection is a rule you enforce in review, not something the tool does.
- Grants are not authenticated.
ratchet grantis an ordinary command, and--bydefaults togit config user.nameand is whatever you type. What stops an agent is layered: the Claude Code integration putsgranton the agent’s deny list,ratchet check --basein CI refuses a raised ceiling with no matching grant in the log, and the grant is a visible line in the diff. Without the CI check, an agent that edits.ratchet.jsonby hand passes locally. - By default it blocks agents, not you. Your own commits go through unless you install the git pre-commit hook, which the docs call strict mode.
- Tightening is manual. Freed room stays available until someone runs
ratchet tighten. In this repo that is one line, which is nothing. After a big cleanup it would not be. - Only listed extensions count. This repo’s source group is
.py. A language you do not list is invisible to the budget. - It cannot tell good code from bad. It knows the count went up. That is the point, and also the reason it has to sit next to review and never replace it.
Try it
- Code and docs: github.com/krishansubudhi/ratchet
- Package: pypi.org/project/ratchet-size
- The raw numbers behind the chart: ratchet-history.csv
The README has a replayed agent run: a commit blocked by one line, the agent stopping to ask instead of cutting code, and the commit landing once a human grants it.
