Skip to main content

ratchet

CI

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.

ratchet refusing a duplicated-code change on pallets/click, then passing once the duplicate is removed

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

  1. Install: pipx install ratchet-size (or pip install ratchet-size)
  2. Baseline: run ratchet init anywhere inside the repo; it writes .ratchet.json at 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.
  3. 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.json by 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.
  • slack keeps some headroom above the measured size after init and tighten, 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)

Source distribution for ratchet-size 0.2.1
File Size Uploaded
ratchet_size-0.2.1.tar.gz 29.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ratchet-size 0.2.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.2.3

2 release files

0.2.2

2 release files

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page