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, orpip install -e .from a clone). - Record today's size:
ratchet init-- writes.ratchet.json; commit it. - 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"
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 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.
What the agent sees when refused
ratchet: REFUSED -- 2 problems (source 10,412/10,300, tests 4,980/5,000)
1. source is 10,412 lines, ceiling 10,300: 112 over
grew vs HEAD: app/api.py +80, app/store.py +40
fix: remove at least 112 lines of source: delete dead code, reuse what
exists instead of adding a parallel version, simplify.
2. app/api.py is 431 lines, limit 400
seams near the middle: line 198 `def upload`, line 230 `class Session`
fix: split it at a seam: move a cohesive group of definitions into a new
file, so each piece is under 400 lines.
Do not edit .ratchet.json or .ratchet-grants.jsonl to get past this.
If the growth is truly needed, stop and ask a human to run:
ratchet grant +112 --group source --reason "<why>" --by <name>
ratchet grant +31 --file app/api.py --reason "<why>" --by <name>
Every refusal says what grew, by how much, and which fix applies: shrink,
split, or ask a human. 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)" --by alice
ratchet grant +60 --file app/api.py --reason "until the router split lands" --by alice
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). 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 + git hook / test-cmd: integrations/other-agents.md |
| pre-commit | 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.1.0
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.1.0.tar.gz | 25.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ratchet_size-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.8 kB
Release files / ratchet_size-0.1.0.tar.gz
| Download URL | ratchet_size-0.1.0.tar.gz |
|---|---|
| Size | 25.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
67c906d4d5ac44804370209e65cc47028c986beff389d699060580e276ec0927
|
|
BLAKE2b-256 checksum How to use checksums |
68c4fe3590f8cfa1f074208cefec4adaf5633115e0689164c108fc37921ce4d8
|
| 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 6, 2026.
Transparency logRelease files / ratchet_size-0.1.0-py3-none-any.whl
| Download URL | ratchet_size-0.1.0-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7c2ced40680d83d851b12f50e3c669b9eec8b6fea2ec1ea126fc8e702a947b7
|
|
BLAKE2b-256 checksum How to use checksums |
6b0a045840fc132c6978d5c135ede3afef179b989af58395e9873050493c514e
|
| 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 6, 2026.
Transparency log