Coding agents are good at adding code and bad at deciding not to.
That was my problem in one sentence. Each change Claude made was locally correct. Each addition was a few lines. Nothing ever got deleted. Then one day the codebase was much bigger than I had meant it to be, and there was no commit I could point at, because there was never one decision. It was a hundred small yeses that nobody actually said.
What I tried first
The obvious fix is to ask nicely. Put "keep it small" and "reuse existing helpers" in the instructions. It works for a while. There is no number, so there is nothing to be over.
The other obvious fix is review. But review sees one diff at a time, and every diff looks fine on its own. Growth is never the fault of a single change, so a reviewer looking at a single change never catches it.
What I wanted was a spending limit. Not "no code". A feature that needs six hundred lines should get six hundred lines. I just wanted someone to say yes out loud, with a reason written down, and for nothing else to be able to raise the number.
So I built ratchet: a code-size budget for your repo that only goes down.
How it works
There are three parts.
The budget. ratchet init measures the repo and writes .ratchet.json, which holds a ceiling for each group, source and tests. Only code files count. Blank lines don't, and neither do docs. By default the ceiling is exactly today's size, so the first change that grows the code is refused. You can pass --slack to keep some room.
The check. ratchet check runs in CI, in a git hook, or inside the agent's session. If a group is over its ceiling it exits 1 and says what grew, by how much, and in which file. The refusal is written for an agent:
ratchet: refused -- source grew 56 lines past its limit (556 / 500) app/api.py +40 app/store.py +16 Fix it one of two ways: 1. remove 56 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 +56 --group source --reason "why" Do not edit .ratchet.json or .ratchet-grants.jsonl to get past this. The grant. ratchet grant +56 --group source --reason "..." raises the ceiling and appends a line to .ratchet-grants.jsonl with who ran it and why. You commit both files together, so every raise shows up in the diff.
A few more rules sit around those three. A single file over 400 lines (the default) is refused too, and ratchet points at the top-level definitions near the middle as places to split. Files already over the cap on day one are frozen at their size: they can shrink, never grow. Shrinking does not lower the ceiling on its own. If you delete fifty lines, that room is yours to spend later, until someone runs ratchet tighten, which pulls every ceiling down to the measured size. Nothing ever raises a ceiling automatically.
The last piece was about when the agent finds out. The usual failure is that it writes the whole change, runs the check at the end, gets refused, cuts some lines, gets refused again, and burns a turn each time. ratchet budget prints current vs ceiling and the headroom left. The Claude Code plugin shows it at the start of a session and warns after any edit that goes over, so the agent knows the limit before it writes the first line.
Does it actually work?
I ran a small experiment: 9 real feature tasks on 3 popular Python repos (click, itsdangerous and typer), each done twice by Claude, the same model and the same prompt, once without ratchet and once with it.
With ratchet, Claude wrote a median of 4 net source lines per task, against 8 without. 8 of the 9 tasks came in smaller and one tied. It did this by reusing existing helpers and trimming docstrings, not by skipping work: every task passed its repo's test suite and my own correctness check in both arms, 9 out of 9.
It was not free. With ratchet the agent took about 2.4x the turns, 2x the cost, and roughly 6x the wall-clock time, because it has to run the check, read the refusal and trim. In 1 of the 9 tasks it ran close to its turn limit on an already-oversized file and gave up without asking for a grant. Nine tasks, one model, and I picked the tasks, so treat it as a sign, not proof.
I also wanted to know whether agents bloat code in the wild, so I replayed a year of main-branch history on five busy repos that use agents heavily: mlflow, pydantic-ai, Sentry, Home Assistant and PostHog. That is 79,866 commits. The honest answer is "a bit". Pooled, the median merged PR was 28 net lines for agent-marked commits and 20 for human ones. Among 207 developers with at least five of each kind, 149 had bigger commits when an agent was involved. But mlflow went the other way, and agent commits did not copy existing code verbatim more often than humans. And a strict ratchet would have refused 75% of agent commits and 73% of human ones. On a fast-growing repo it is a tax on all growth, not an agent detector. That is fine with me. The point is the yes, not the no.
Living with it
Ratchet runs on its own repo in CI. Its first commit had 690 source lines. At launch it had 763 source lines against a ceiling of 764, and 322 test lines against 322. The grants log holds two lines, +54 source and +33 test, from one commit that added a budget command and two hooks.
Those two grants also showed me a trap: each one landed the ceiling exactly on the measured total, leaving zero headroom. I would rather say that than hide it.
The limits
- It counts lines, not quality. A dense rewrite passes. Run it after your formatter, or agents will just squeeze lines.
- Tests count, so deleting a test frees room. Ratchet can't tell a dead test from an important one. That is for review.
- Grants are not authenticated. What stops an agent is layered: the Claude Code integration denies the agent
grant,ratchet check --basein CI refuses a raised ceiling with no matching grant, and the grant shows in the diff. - By default it blocks agents, not you. A git pre-commit hook makes it strict.
- Tightening is manual.
It doesn't make code better on its own. It makes every increase a conscious yes.
Try it
pip install ratchet-size ratchet init # records today's size as the ceiling ratchet check # passes now; refuses once code grows past it Nothing is enforced until you wire up a hook or CI. It is pure stdlib, Python 3.10+, Apache-2.0, and new, so expect rough edges.
The code and docs are at github.com/krishansubudhi/ratchet. Feedback welcome.

