Post

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.

Ratchet: a code-size budget for your repo that only goes down.

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.json holds a ceiling for each group, source and tests. Only code files count. Blank lines don’t, and neither do docs.
  • The check. ratchet check runs 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.jsonl with 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.

edit code (agent or you)ratchet checkover the ceiling?noexit 0commityesexit 1: refusedagent stops and asksratchet grant, with a reasonthen commits both filesdiff shows the raiseagent cuts linesIn CI, ratchet check --base refuses any raisedceiling with no matching grant in the log.
each click = a line removedno grant, no turning back

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.

ceilingmeasuredsource lines680700720740760780grant +54763 measured / 764 ceilingtest lines260280300320340grant +33322 / 322Oct 6Oct 7 grantOct 11
Measured lines per commit against the ceiling in effect, 32 commits, read from the ratchet repository.
  • 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 grant is an ordinary command, and --by defaults to git config user.name and is whatever you type. What stops an agent is layered: the Claude Code integration puts grant on the agent’s deny list, ratchet check --base in 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.json by 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

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.

A coding agent is blocked by ratchet's git hook for one line over the ceiling, stops and asks the human, who runs ratchet grant, and the commit then passes

This post is licensed under CC BY 4.0 by the author.