weeder

the judge

tests still mean what they meant.

weeder reads the diff an agent just wrote and refuses the cheap road to green: a deleted test, a dropped assertion, a skip, a stub, a swallowed error, a secret, a guardrail edited. one static binary, milliseconds, no model in the loop.

rust · exit 2 blocks · every finding as sarif

weeder · zsh
~/project $ weeder check
error  T1  src/parser.test.ts  a test file was deleted, and 2 test cases went with it.
1 error, 0 warnings, 0 notes

~/project $ weeder guard install
weeder guard is installed: git runs its hooks from .githooks.
pre-commit  refuses an index that carries a finding weeder blocks on
commit-msg  refuses an index that blocks, where the message allows none of what it blocks on
pre-push    refuses a pushed range that blocks, and a non-fast-forward to a protected branch
pre-rebase  refuses a rebase that would rewrite a protected branch
protected: main, master (as weeder.toml has them today).

why weeder

an agent is the worst judge of the diff it just wrote

it is asked for green and it will find green. the shortest road there is almost never the code.

the failure

one line, and the suite forgets

a red test is deleted. an assertion goes quiet. a skip marker appears. an exception is caught and dropped on the floor. a timeout widens until the flake stops. each one is a single line, and each one is the last thing a reader finds in a diff of a thousand.

why not a model

the same judgement on both sides of the desk

asking a model to review what a model produced puts one kind of judgement in both chairs. it is also slow, priced per token, and different every time you ask. a gate has to answer the same way twice.

what weeder does

reads the change, not the code

weeder compares what is there now against what was there before and reports shapes: a case that is gone, an assertion that was dropped, a stub where an implementation belongs. no prompt, no score, no opinion about style. an exit code, and a sarif log for the pull request.

what it judges

twenty seven rules, and only the unambiguous ones block

every rule has an id, an adversarial fixture in typescript, python, rust and go, and a level. a block stops the change. a warning is for the person at the pull request, never for the agent to argue with.

block the change does not go through

  • T1a test was deleted
  • T2assertions were dropped from a changed test file
  • T3a skip or focus marker was added
  • T7a rename took a test out of the runner
  • T8a configuration line took a test out of the run
  • S1a stub or a todo reached production code
  • D2an import crossed a forbidden boundary
  • X1a secret-looking string was added
  • X2a file outside the scope was touched
  • C1a guardrail file was edited
  • G1a conflict marker was committed
  • B1a test passed without the change it covers

warn for the human reading the diff

  • T4a tolerance or a timeout was widened
  • T5expected values were regenerated
  • T6an error assertion was weakened
  • M1a test mocks the unit under change
  • S2an error was swallowed
  • S3a debug leftover reached production code
  • D1a dependency manifest changed
  • C2an ignore file was broadened over source or tests
  • C3a workflow was changed
  • G2a large or a binary file was added

scan the tree as it stands, never blocking

  • R1the docs cite something that no longer exists
  • R2a public symbol has no references
  • R3a todo is older than the configured age
  • R4a dependency pin lags the registry
  • R5a test file the configuration never collects

where it runs

the gate belongs where the work lands

a rule nobody runs is a preference. weeder is wired into the four places a change passes through on its way in.

in git

guard

weeder guard install writes the hooks and points core.hooksPath at them. pre-commit judges the index, pre-push judges the range and refuses a rewrite of a protected branch. it holds for every agent and every human alike.

in the harness

the stop hook

weeder hook answers a harness's hook event on stdin and replies in the shape that harness reads. the turn ends against weeder's verdict rather than against the agent's own account of it.

in the plan

pleach's gate

pleach runs agent work as a graph of gated nodes. weeder is the gate on each one, so a node whose diff blocks does not publish, and the plan never merges a step that got green the cheap way.

on the pull request

the action

in ci, weeder check judges the pull request against the branch it opens onto and writes sarif. code scanning takes the log as it is, so every finding lands as an annotation on the diff a reviewer is already reading.

measured, 2026-09

a gate is only worth having if it is almost never wrong

weeder was run over real history at pinned commits, every block was classified by hand with one line of reasoning, and a second party re-graded the classification blind.

635commits of real history judged, in 5 repositories, each against its own parent
20commits blocked, of which 4 were block-level false positives
0.63%false-positive share of the commits judged, under the two percent bar
2877of 2907 planted anti-patterns caught, each on the site it was planted in

recall holds per rule and per language. every rule that blocks stays at or above 95 percent in each of typescript, python, rust and go. the cases are planted one anti-pattern at a time into real commits by a scanner that knows nothing about weeder's detectors, and a site the commit already fired on is passed over, so no hit is inherited.

the classification is a second party's. the blind re-grade judged each case from a packet with the builder's class out of sight, and agreed on 18 of 20 blocked commits and 19 of 20 recall cases. the false-positive floor, 0.94 percent, takes every verdict it called wrong as true.

both readings stay in the report. under the three classes the ledger used first, the same 23 blocked commits counted 7 false positives, 1.10 percent. under the sharper question, is what the finding says true of the change, they count 4, 0.63 percent. the report keeps both, so the change of definition is on the record beside the number.

the corpus, the pins, the judgement of every block and the script that checks the arithmetic are in the repository, at docs/calibration-2026-09.md. a commit pushed to any of those sources after the pin falls outside the window and cannot move a number here.

honest scope

what weeder decides, and what it leaves alone

on its own

one binary, one command

install it, run weeder guard install, and the repository has a gate. nothing else in the garden has to be there, no service runs, and no token is spent.

with the garden

tend2 and pleach

tend2 holds the map and only its verifier writes a pass. pleach runs the work as gated nodes. weeder is the gate they call, so a loop's checks stay honest because the diff that would have weakened them never lands.

beside what you already run

with the linter and the scanner

a linter reads style, a scanner reads for secrets, a review bot reads the shape of a pull request. weeder reads the change against the tests and the guardrails, and says only what it can prove. it replaces none of them and argues with none of them.

what it will not say

whether the code is good

weeder has no view on style, structure, naming, or whether the feature was worth building. it decides one thing, and says so in an exit code: whether the tests still mean what they meant.

start

install it, then put it inside git

the binary is static and the install is a tarball with its digest beside it. release binaries are built for linux and macos on x86_64 and aarch64; windows is on its way, and the crate and the npm wrapper publish from the same workflow once the registries are set up. the last line is the one that matters: after guard install the gate runs whether anyone remembers it or not.

fetch the release for your platform with its digest, and check it (aarch64-apple-darwin, x86_64-apple-darwin, x86_64-unknown-linux-musl or aarch64-unknown-linux-musl)t=aarch64-apple-darwin; b=https://github.com/jahala/weeder/releases/latest/download; curl -fsSLO "$b/weeder-$t.tar.gz" -O "$b/weeder-$t.tar.gz.sha256" && shasum -a 256 -c "weeder-$t.tar.gz.sha256"
unpack the executable and put it on your pathtar xzf "weeder-$t.tar.gz" weeder && install -m 755 weeder ~/.local/bin/weeder
then put the judgement inside gitweeder guard install --protect main