Your AI agent says it’s done.
Nothing checked.
plumb reads a repository, finds the verification commands it declares, and runs one before anything reaches CI. It makes no model calls and sends nothing anywhere. The same repository gives the same answer, whichever agent wrote the change.
Then it tells you what that established — and what it did not. plumb has five verdicts and three of them are refusals: a check could not run here, running it settled nothing, or your repository declares no check at all.
And when it cannot establish something it says whose limitation that is — your repository’s, this machine’s, or plumb’s own.
curl -fsSL https://plumbing.sh/install | shmacOS arm64 · Linux x86-64 · Linux arm64 · Windows through WSL
Move what does not require model judgment out of model judgment.
No model calls. No telemetry. Verify it yourself — run plumb with the interface down, or under tcpdump.
plumb ran on this website and rejected it.
Not a staged example. This site declares one check in its package.json. plumb found it, ran it, and it failed — naming five locations.
npm run lintPLUMB ~/Desktop/plumbing-site
26 files · 4 declared targets · 21ms · discovery via git-ls-files
FOUND 1 verification command
✓ npm run lint package.json
RESULTS
FAIL npm run lint 1.0s
→ src/app/deck.tsx:17
→ src/app/layout.tsx:54
→ src/app/pricing/page.tsx:6
→ src/app/privacy/page.tsx:6
→ src/app/security/page.tsx:6
VERDICT FAIL — 1 executed; a repository-declared check rejects this tree.
Four of those were <a href="/"> where Next.js requires <Link>, and one was an unused constant. Real defects, shipped for weeks. Fixed, then run again:
Two seconds, locally, before a commit. The same failure would have taken a CI round trip to find — after the agent run was already paid for.
PLUMB ~/Desktop/plumbing-site
26 files · 4 declared targets · 37ms · discovery via git-ls-files
FOUND 1 verification command
✓ npm run lint package.json
RESULTS
PASS npm run lint 0.8s
VERDICT PASS — all 1 runnable check passed.
Context first. Work second. Verification last.
plumb context
emit the checks, source locations, and repository obligations before the agent edits
agent writes
keep the agent you use today; plumb is not another model and does not write code
plumb check
run a declared verification command and report what the result establishes
plumb context emits what a repository declares — every check, where it is declared, whether this machine can run it, and the obligations its documents state. Here it is on spf13/cobra, a repository plumb has never been tuned on.
Every line carries a file and a line number, so an agent can check the claim instead of trusting it. BLOCKED means this machine lacks the toolchain — a fact about the machine, not the repository.
[PLUMB]
CANONICAL: make test [Makefile:22] BLOCKED: binary:go (called by make test)
CANONICAL: make lint [Makefile:18] BLOCKED: binary:golangci-lint (called by make lint)
CANONICAL: go vet ./... [go.mod] BLOCKED: binary:go
CANONICAL: go test ./... [go.mod] BLOCKED: binary:go
MUTATOR: make fmt [Makefile:14] BLOCKED: binary:gofmt (called by make fmt)
VARIANT: make all [Makefile:12] BLOCKED: binary:go (called by make all);…
REQUIRED: Run `make all`. [CONTRIBUTING.md:36]
REQUIRED: Create your feature branch (`git checkout -b my-new-feature`) [CONTRIBUTING.md:42]
REQUIRED: Add them to staging (`git add .`) [CONTRIBUTING.md:44]
Agents can write code. Finishing is a different problem.
Context is a guess
An agent may or may not find the CONTRIBUTING file, the Makefile target, or the CI workflow that decides whether the change is acceptable.
Self-review is not review
Asking the model that wrote the change whether the change is done produces an opinion, not evidence.
CI is the wrong place to learn
By the time CI rejects it you have paid for the agent run, the review, and the round trip.
Five verdicts, and three of them are refusals.
a declared check ran and accepted the tree
a declared check ran and rejected it, with the failing locations
the check exists; this machine cannot run it
it ran, and the result does not settle the question
this repository declares no command that verifies a change
The last three exist because a verifier that cannot say “I don’t know” will eventually say something false instead. A missing Go toolchain is a fact about your laptop; plumb will not turn it into a claim about your code.
What it will tell you in twenty seconds.
Reading costs nothing and needs no license. Point it at a repository you already have and it answers three things you probably cannot answer from memory:
Every command, where it is declared, and which ones are blocked because a toolchain is missing here.
The obligations its CONTRIBUTING and AGENTS files state, quoted with a file and a line.
A contributor following the docs would never learn these exist.
If the answer is “this repository declares nothing”, plumb says that too, and says it is a statement about the repository rather than a failure of the tool.
what has been tested, and what it does not establish →It does not need a service to work.
One exception, named rather than buried: plumb update fetches a signed manifest to see whether a newer release exists. It asks before it ever does, declining buys silence, and nothing else in plumb has a reason to reach anywhere.
A license is a signed file, verified on your machine against a key compiled into the binary. There is no license server, so there is nothing to be unreachable. A license can be carried onto an air-gapped machine on a USB stick and it verifies there.
You do not have to take that on trust. Run plumb with Wi-Fi off, or watch the active interface with tcpdump. Source code would be weaker evidence for a runtime claim than the test you can run yourself.
Keep your agent.
plumb is not another model and does not write code. It hands an agent a deterministic map of the repository, and afterwards runs a check the repository itself declares. Registered with plumb init, it runs when the agent says it has finished — and blocks only on FAIL, so a missing toolchain reports BLOCKED and lets the turn end.
No flags, and no agent to name. It merges into configuration files you already own rather than replacing them, and plumb init --undo removes exactly what it added.
wrote .claude/settings.json new file
wrote .codex/hooks.json new file
plumb now runs when an agent says it is done, in tmp.rqxqjIA9bn.
It blocks only on FAIL -- a missing toolchain reports BLOCKED and lets the turn end.
One check, up to two minutes. Raise it with PLUMB_HOOK_TIMEOUT if your suite needs longer.
What you are probably thinking.
- Can’t I just tell my agent to run the tests?
- Yes — if you already know which commands matter, and you are willing to repeat that for every repository you touch. plumb is for the step before that: finding what this repository declares, where the requirement came from, whether it can run here, and what the result actually establishes. On a project whose whole story is one test script, you do not need it.
- Isn’t this just a hook?
- A hook decides when something runs. It has no opinion about what your repository requires — you still have to tell it, and keep telling it as the repository changes. plumb answers that question, and installs into the hook you already have.
- What if my repository declares nothing?
- Then plumb says so, plainly, and runs nothing. That is a real outcome and it is common — five of the author’s own seven projects return exactly that. You will find out in about twenty seconds, and finding out is free.
- It is a closed-source binary. Why would I trust it?
- Do not. Test it. Run plumb with Wi-Fi off, or watch the active interface with tcpdump while it works. The central claim — that it makes no model call and no network request — is the rare one that is stronger tested than read. Source code would be weaker evidence for a runtime claim than the ten seconds it takes you to check.
Reading is free. Running costs money.
plumb, rules, coverage, explain, context and init execute nothing and are free permanently. plumb check executes a command, and that is the paid one.
curl -fsSL https://plumbing.sh/install | sh