CLI Reference

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.

$ npx bramo-verify

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.

git clone --depth 2 https://github.com/KiniunCorp/bramo-verify-demo demo && cd demo && npx --yes bramo-verify

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.

$ npx bramo-verify

Or add it to a project, so everyone on it runs the same version:

npm i -D bramo-verify

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.

bramo-verify
HEAD~1..HEAD — the last commit. The default: “what the agent just did.”
bramo-verify HEAD~3
The last three commits.
bramo-verify main
The whole branch, against its merge-base with main — so commits main picked up after you branched don’t pollute the diff.
bramo-verify --staged
Staged changes only, for pre-commit use.

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:

Node
package.json scripts.test — npm/yarn/pnpm/bun, chosen by lockfile
Python
pytest.ini, tox.ini, [tool.pytest], or pyproject.toml + tests/
Django
manage.pypython manage.py test
Go
go.modgo test ./...
Rust
Cargo.tomlcargo test
Elixir
mix.exsmix test
Ruby
Gemfile + spec/ → RSpec; or a Rakefile
JVM
pom.xml → Maven; gradlew → Gradle
PHP
composer.json scripts.test
Make
A test: target — last resort, only when nothing native matched

Exiting 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.

placeholder
TODO, FIXME, XXX, HACK in a comment, outside test files
empty-function
A named function whose body is empty
bare-constant-return
Takes arguments, ignores them, returns a constant
hardcoded-lookup
Answers the inputs it knows, invents a constant for the rest
test-not-wired
Test files added that your test command never actually runs

JavaScript 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:

// legitimate — the lookup is the whole function
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.

FACTS
Observed directly — a real spawned process, its real exit code, the file count from git. These cannot be false positives.
FINDINGS
Heuristics, each cited file:line so you can check it in seconds. Dismiss any that don’t apply.
CLEAN
Tests ran, passed, verified something real; nothing flagged.
UNVERIFIED
Nothing failed, but nothing was confirmed either.
FINDINGS
Heuristic findings present.
FAILED
Objective failure: the test suite ran and failed.

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

0
Passed under the current --fail-on policy
1
Heuristic findings
2
Objective failure — the test suite ran and failed
3
Nothing could be verified (only with --fail-on all)
64
Could not audit at all — not a git repository, bad ref, unknown flag

Codes 64 and above mean bramo-verify could not run. Everything below means it ran and produced a verdict.

Flags

--staged
Audit staged changes instead of a commit range.
--out <dir>
Where to write the verdict JSON. Default .bramo/verdicts.
--no-write
Don’t write a verdict file at all.
--json
Print the verdict as JSON on stdout.
--quiet
Print one summary line only.
--no-color
Disable colour. Also honours NO_COLOR, and turns itself off when output is piped.
--fail-on
facts · findings (default) · all · never. Which results produce a non-zero exit.
--install-hook
Install a pre-push hook that runs this on every push.
--uninstall-hook
Remove that hook.

Hooks and CI

A tool you have to remember to run gets run once and forgotten. Both of these fire on their own.

$ bramo-verify --install-hook

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:

npx bramo-verify origin/main --fail-on facts

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.

generatedAt
Local time with UTC offset. Local so two runs a minute apart never straddle a UTC day; offset so it stays unambiguous elsewhere.
range.mode
range · staged · hook
range.baseline
The resolved baseline actually audited — the merge-base, not necessarily the ref you typed.
range.empty
The audit ran fine but the diff was empty.
facts.test.ecosystem
Which convention matched: node, python, go, …
facts.test.exitCode
The real exit code of the spawned process.
facts.test.passed
true only if it exited 0 and executed at least one test; null when the run was vacuous.
facts.test.testCount
Parsed from recognised runner output, or "unknown" — never guessed.
facts.test.vacuous
Exited 0 but verified nothing. This is the fake-test-script shape.
factFailures
Objective failures. The suite ran and failed. Cannot be false positives.
factGaps
Objective gaps. Nothing could be verified — not failures. A repo with no tests yet is not a broken repo.
findings
Heuristics: kind, file, line, detail.
verdict
CLEAN · UNVERIFIED · FINDINGS · FAILED
summary
One plain-English line for humans.

The 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.