ratchet
A code-size budget that only goes down. Coding agents are good at adding
code and bad at deciding not to. ratchet gives the repo a line budget:
growth past the ceiling is refused, in plain words the agent can act on, and
only a human can raise it.
Real run, on pallets/click: ratchet init
records today's size; an agent asked for a feature pastes a near-duplicate of
existing code instead of reusing it; ratchet check refuses the growth and
says exactly why; the duplicate goes, and the check passes again. Real
commands and output: docs/examples.md. (The bloat step
is a scripted patch standing in for the agent, honestly -- see the caption
there.)
Quickstart
- Install:
pipx install ratchet-size(orpip install ratchet-size) - Baseline: run
ratchet initanywhere inside the repo; it writes.ratchet.jsonat the repo root; commit it. Ratchet works without git too: outside a repo it walks the directory; only strict mode,--base, and the "grew vs HEAD" breakdown need git. - Wire up your agent: paste this sentence to it: "Set up ratchet in /path/to/repo by following https://github.com/krishansubudhi/ratchet/blob/main/SETUP-FOR-AGENTS.md" Ratchet then refuses the agent when it tries to finish with code over the ceiling; it trims or asks you. Your own commits are not blocked.
Strict (optional): also block your own commits. Opt in with the git pre-commit hook:
curl -o .git/hooks/pre-commit https://raw.githubusercontent.com/krishansubudhi/ratchet/main/integrations/git/pre-commit && chmod +x .git/hooks/pre-commit
What you sign up for: any commit that grows code past the ceiling is refused
until you delete code or a human runs ratchet grant +N --reason ....
Only code files count (.py, .js, .ts, .go, ...) -- README and docs
edits never count.
That's it. Everything past this point is detail you can come back to: what counts as a line, the four rules, grants, config, and per-harness integrations.
Claude Code plugin
Prefer a plugin over the manual setup above? Ratchet ships as a Claude Code plugin, with this repo doubling as its own marketplace:
/plugin marketplace add krishansubudhi/ratchet
/plugin install ratchet@ratchet
That installs a SessionStart hook that prints the budget into context, a
PostToolUse meter that warns the moment an edit pushes a bucket over, and
a Stop hook (ratchet check --hook, falling back to uvx if ratchet
isn't on PATH yet) so Claude Code can't end a turn with the repo over
budget -- plus a /ratchet:setup command that runs the
agent setup -- install, ratchet init, detect your
harness, ratchet check -- in the repo you're in. See
.claude-plugin/ and hooks/hooks.json
for the manifests, or claude plugin validate . to check them yourself.
What it counts
Scope: the one git repository you run it in (git rev-parse --show-toplevel). Other repos on the machine are never touched, and each
repo gets its own .ratchet.json.
Which files: git-tracked files, plus untracked files that aren't
gitignored, whose extension is one of the code extensions present at
init (.py, .js, .ts, .go, .rs, .java, .rb, .c, .sh, and
more -- see CODE_EXTS in ratchet/measure.py). Blank lines don't count.
Source vs tests: a file matching a test glob (test_*, *_test.*,
*_tests.*, *.test.*, *.spec.*, or living under tests/, test/,
__tests__/, spec/) is tests; everything else is source. Both groups
are plain config in .ratchet.json -- edit include/exclude/extensions
to carve out a different split, e.g. "exclude": ["vendor/**", "**/*.gen.*"]
to stop counting vendored or generated code.
Existing repo: ratchet init measures today's totals and sets the
ceilings there (plus optional --slack). A file already over
max_file_lines is frozen at its current size, so nothing is refused on
day one -- you start where you are. From then on a ceiling may only shrink,
unless a human runs ratchet grant.
Budget
The usual failure mode for an agent is to write the whole change, run the
check at the end, get refused, cut some lines, get refused again, and repeat
-- burning a turn each time on something it could have seen coming.
ratchet budget is the fix: it prints current vs ceiling, and remaining
headroom, per group, plus any file that's near its own per-file cap, without
running a single test:
$ ratchet budget
ratchet budget -- source 10,188/10,300 (112 left), tests 4,950/5,000 (50 left)
near the per-file cap:
app/api.py is 390/400 (10 left)
ratchet budget --json gives the same numbers as data, for a harness.
Two hooks put it in front of the agent without it having to ask:
- A SessionStart hook prints it into the agent's context at the start of a session, so the ceiling is known before the first line is written.
- A PostToolUse hook (
ratchet budget --hook) runs after every edit and stays silent until a bucket's remaining headroom goes negative, then prints one short warning with the overage -- mid-work, not only at the end.
Both ship with the Claude Code plugin; see hooks/hooks.json
and integrations/claude-code to wire
them up by hand elsewhere.
What a refusal looks like
A human committing through the git hook sees:
ratchet: commit blocked -- source grew 1 line past its limit (1,129 / 1,128)
starter.py +1
Fix it one of two ways:
1. remove 1 line of code (dead code, duplicates), or
2. allow the growth (humans only -- agents must ask, never run this):
ratchet grant +1 --group source --reason "why"
An agent sees the same facts, told to ask rather than grant, and to stop after a second refusal:
ratchet: refused -- 2 problems
1. 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"
2. app/api.py is 20 lines past its limit (420 / 400)
seams near the middle: line 313 `def upload`, line 316 `class Session`
Fix it one of two ways:
1. split it at a seam: move a cohesive group of definitions into a new file, each under 400 lines, or
2. stop and ask a human to allow the growth (humans only -- never run this yourself):
ratchet grant +20 --file app/api.py --reason "why"
Do not edit .ratchet.json or .ratchet-grants.jsonl to get past this.
Refused again on the same change? Stop here: report these numbers to a human instead of cutting more and resubmitting.
Agent wording is used under check --hook, when RATCHET_AGENT=1, or when
a known agent's variable is set (CLAUDECODE, CURSOR_AGENT, GEMINI_CLI);
RATCHET_AGENT=0 forces the human wording. "commit blocked" replaces
"refused" when run from a git hook. For a harness, ratchet check --json
gives the same result as data.
The four rules
| Rule | What it does |
|---|---|
| Budget | Each group (source, tests by default) has a ceiling. A total over it is refused. |
| Seams | A file over max_file_lines (default 400) is refused, with the top-level definitions nearest the middle offered as places to cut. A file already over the limit at init is recorded at its size: it may shrink, never grow. |
| Grants | Only ratchet grant raises a limit, and every grant is appended to .ratchet-grants.jsonl with who and why. |
| Tighten | ratchet tighten lowers every ceiling to the measured size (plus optional slack). Nothing ever raises it automatically. |
Lines are counted without blanks. Docs, data, and fixtures don't count: only
files whose extensions you list in .ratchet.json do.
How grants work
Sometimes the growth is the point: a real feature, a new integration. Then a human runs:
ratchet grant +300 --group source --reason "CSV export (issue 41)"
ratchet grant +60 --file app/api.py --reason "until the router split lands"
--by defaults to git config user.name (else $USER); pass it to override.
That raises the ceiling in .ratchet.json and appends a line to
.ratchet-grants.jsonl:
{"after": 10600, "before": 10300, "by": "alice", "lines": 300, "reason": "CSV export (issue 41)", "target": "source", "ts": "2026-01-15T09:30:00Z"}
Commit both files. Here is what keeps grants honest:
- In CI, run
ratchet check --base <target branch>. It refuses any ceiling that is higher than the base's unless grants appended since the base pay for it. An agent that edits.ratchet.jsonby hand passes locally and fails review. The check also refuses a grants log that was rewritten rather than appended to. - Grants are one-line diffs in the pull request, so a reviewer sees them.
- Tell your agent never to run
ratchet grant. The Claude Code integration also puts it on the permission deny list.
Run ratchet tighten after a cleanup and commit the result. The room you
freed is now the new ceiling.
Running it by hand
ratchet check exits 0 when the repo is within budget, 1 when refused,
2 on a setup error (bad config, not a git repo when --base is used, and
so on). ratchet budget always exits 0 -- it's a report, not a gate --
except in --hook mode (see above), which exits 2 the moment headroom
goes negative. python -m ratchet works the same as the ratchet entry
point, if you don't have it on PATH.
Integrations
| Harness | How |
|---|---|
| Claude Code | Stop hook ratchet check --hook: integrations/claude-code |
| Cursor, Codex CLI, aider | instruction file (+ opt-in git hook) / aider test-cmd: integrations/other-agents.md |
| pre-commit (opt-in strict) | framework hook, or a plain git hook: integrations/pre-commit |
| GitHub Actions | uses: krishansubudhi/ratchet@v0: integrations/github |
| Anything else | prompt snippet: integrations/any-agent.md |
Configuration
ratchet init writes .ratchet.json. It is JSON because ratchet must write
the file, and Python's standard library reads TOML but cannot write it.
{
"version": 1,
"max_file_lines": 400,
"slack": 0,
"groups": {
"source": {"include": ["**"], "exclude": ["**/tests/**", "**/test_*"], "extensions": [".py", ".ts"]},
"tests": {"include": ["**/tests/**", "**/test_*"], "extensions": [".py", ".ts"]}
},
"ceilings": {"source": 10300, "tests": 5000},
"file_ceilings": {"app/legacy.py": 1210}
}
- Globs work like gitignore:
**/means any number of directories. - Each file belongs to the first group that matches it.
slackkeeps some headroom above the measured size afterinitandtighten, if you would rather not require a delete for every add.- Files are listed with
git ls-files, so ignored files never count. Outside git, ratchet walks the tree and skips the usual build and vendor directories.
ratchet init --max-file-lines 300 --slack 50 sets both at creation.
Requirements
Python 3.10+, git (optional, needed for --base and the "grew vs" lines). No
dependencies. Tests: pip install pytest && python -m pytest -q.
License
Apache-2.0. Copyright 2026 Krishan Subudhi.
Metadata
Release files for ratchet-size 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ratchet_size-0.2.1.tar.gz | 29.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ratchet_size-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.8 kB
Release files / ratchet_size-0.2.1.tar.gz
| Download URL | ratchet_size-0.2.1.tar.gz |
|---|---|
| Size | 29.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34234ba4331bf8122166beebef77956acbb2703d0fdb685255205bebc4a1a8eb
|
|
BLAKE2b-256 checksum How to use checksums |
f1cf1105c4696034f52d57eb75b5e66911225517899c4393db10a5487270ebdb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logRelease files / ratchet_size-0.2.1-py3-none-any.whl
| Download URL | ratchet_size-0.2.1-py3-none-any.whl |
|---|---|
| Size | 24.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
53c5bec006f38264f0b629b286139989c8e6c40d1f2a56ba948f9ca5045aacac
|
|
BLAKE2b-256 checksum How to use checksums |
e0e77cfbfb27ffe39c1e534c3627d7539d5d06be0dceb45e71eb089c65e9d30f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency log