bramo-verify
Audits a git diff and reports what it observed — never what an agent claimed. Your coding agent says the tests pass; bramo-verify spawns them and reads the real exit code. It says the feature is done; bramo-verify scans the diff for the shapes unfinished work leaves behind. Free, local, no account, no API key, no telemetry.
Try the demo
If you would rather not point an unfamiliar CLI at your own work first, there is a planted repository built for exactly that. It contains one commit an agent claimed was finished, and two things wrong with it.
Expect the verdict FINDINGS and exit code 1. The demo README explains what is planted and why each one is the kind of thing that survives review.
The clone needs --depth 2, not --depth 1. bramo-verify audits HEAD~1..HEAD by default, so a single-commit clone gives it no baseline to diff against. For the same reason, a tarball download without git history will not work.
Install
Requires Node 20 or newer. No account, no API key.
Or add it to a project, so everyone on it runs the same version:
Then npx bramo-verify, or npx bramo-verify --install-hook to have it fire on every push.
Usage
The argument is the baseline to compare against. HEAD is always the other end, so a bare ref alone is never the target — a diff needs two endpoints.
HEAD~1..HEAD — the last commit. The default: “what the agent just did.”main — so commits main picked up after you branched don’t pollute the diff.What it checks
Did the tests really run?
bramo-verify finds your test command, spawns it as a real child process, and reports the exit code it actually saw. Nothing here is inferred from text an agent produced. Recognised automatically, first match wins:
package.json scripts.test — npm/yarn/pnpm/bun, chosen by lockfilepytest.ini, tox.ini, [tool.pytest], or pyproject.toml + tests/manage.py → python manage.py testgo.mod → go test ./...Cargo.toml → cargo testmix.exs → mix testGemfile + spec/ → RSpec; or a Rakefilepom.xml → Maven; gradlew → Gradlecomposer.json scripts.testtest: target — last resort, only when nothing native matchedExiting 0 having run zero tests is reported as inconclusive, not passed. A test command that prints reassuring text and exits 0 without executing anything is one of the most common ways work looks finished when it isn’t. Counts come from recognised runner output; when nothing recognisable is printed the count is unknown — never a guess.
Does anything look unfinished?
A scan over the lines the diff added. These names appear verbatim in the output beside each file:line.
TODO, FIXME, XXX, HACK in a comment, outside test filesJavaScript and TypeScript (including JSX and TSX) are parsed, so these are structural findings rather than text matches: a TODO inside a string literal isn’t flagged, and a constant return separated from its brace by a comment still is.
What is deliberately not flagged
Two checks are narrower than they could be, because a false accusation is worse than a missed stub for a tool whose whole job is trust. Functions returning null or undefined are never flagged — deliberate no-ops and unset defaults are legitimate and common. And only named declarations are scanned for empty bodies, not inline callbacks like .catch(() => {}).
The same reasoning shapes hardcoded-lookup. A lookup function is its lookup, so the first of these is never flagged and the second is:
return STATUS_TEXT[code] ?? 'Unknown';
// flagged — answers what it knows, invents the rest
if (text in KNOWN) return KNOWN[text];
return 0;
Reading the output
Output separates two kinds of information that must never look equally certain.
file:line so you can check it in seconds. Dismiss any that don’t apply.Precedence, highest first: FAILED > FINDINGS > UNVERIFIED > CLEAN. Any finding forces at least FINDINGS — the anti-stub scan is a hard veto, not a vote.
Exit codes
--fail-on policy--fail-on all)Codes 64 and above mean bramo-verify could not run. Everything below means it ran and produced a verdict.
Flags
.bramo/verdicts.NO_COLOR, and turns itself off when output is piped.facts · findings (default) · all · never. Which results produce a non-zero exit.pre-push hook that runs this on every push.Hooks and CI
A tool you have to remember to run gets run once and forgotten. Both of these fire on their own.
Installs a pre-push hook that audits the outgoing range on every push. It won’t clobber an existing hook — if one is there it refuses and says so. Remove it with --uninstall-hook; bypass a single push with git push --no-verify.
For CI, run it directly and pick a policy:
Start with --fail-on facts in CI. It fails only on objective failures, so a heuristic finding annotates without red-lighting anyone’s build. Tighten to findings once you trust the signal on your codebase.
Verdict JSON schema
Written to .bramo/verdicts/<date>-<sha>.json on every completed audit — including an empty diff, which is a real result. Suppress with --no-write, relocate with --out. .bramo/ is added to .gitignore automatically so the tool never pollutes your next commit. Current version schemaVersion: 2; fields are additive within a version, a breaking change bumps it.
range · staged · hooknode, python, go, …true only if it exited 0 and executed at least one test; null when the run was vacuous."unknown" — never guessed.kind, file, line, detail.CLEAN · UNVERIFIED · FINDINGS · FAILEDThe split between factFailures, factGaps and findings exists so a consumer — a CI job deciding whether to block a merge — can act on them without parsing prose.
Limits
Findings are signals, not proof. They point at shapes unfinished work tends to leave; a stub written carefully enough leaves none of them. Treat a clean result as “nothing obvious here”, not “this is correct”. The tool is deliberately tuned to miss things rather than to guess — flagging the wrong thing is a bug worth reporting.
It never judges whether the code does what you meant. It has no idea what you meant. It reports what ran and what looks unfinished, and leaves the judgement to you.
Findings are structural for JavaScript and TypeScript. In other languages they come from pattern matching over the diff instead of a parse, so the shape-based checks don’t apply there yet.
A detected-but-missing runner is a gap, not a failure. If your project looks like a pytest project and pytest isn’t installed, you get “tests not run”, never “tests failed”. A missing tool is not broken code.
bramo-verify ships minified with no sourcemaps. That is obfuscation, not real closure — anyone determined can read it. The verification method is documented on this page on purpose: you should be able to judge a verdict without reading the source, the same way an auditor publishes its standards rather than its software.
Part of Bramo — the independent supervision layer for people who build with AI coding agents. Join the waitlist.