Overview
hadome (歯止め) compares a change with main and reports the violations the change adds and the tests it weakens. Violations already on main are not reported.
It looks at how code is written (dependency direction, size, comments) and how tests are written. Tests are compared not by their number but by the functions they call. When deleting a test narrows that scope, it is reported as a regression.
It is meant for repositories where AI agents write most of the code. The result is also available as JSON, which agents read to fix what it reports.
One binary reads Go, TypeScript, Python, Rust, Java, Kotlin, Terraform and Ansible.
Quick start
Run every command at the root of the repository.
1. Install
Pick the one for your language. Each pins the version (distribution). Every way installs the same binary and reads the same .hadome.yml.
From here on, commands are written as hadome …. Use the form for how you installed it, such as npx hadome … or go tool hadome ….
TypeScript / JavaScript
[!NOTE] Publishing to npm is delayed while npm reviews the package names. Until then, use one of the ways under "Anything else" below.
Python
Go
Built from source. With a C compiler it reads all eight languages; without one, Go, Terraform and Ansible only.
go get -tool adds hadome's dependencies to your product's go.mod. Go picks one version of each dependency for the whole module, so your product's dependencies can move up to match hadome's. To avoid that, keep a separate go.mod for tools. Then pass -modfile=tools.mod on every call, and commit tools.mod and tools.sum as you do go.mod.
Java / Kotlin (Gradle)
// build.gradle.kts
plugins { id("io.github.rerorets.hadome") version "0.4.1" }
Run it with ./gradlew hadomeCheck.
Java / Kotlin (Maven)
<!-- in pom.xml, under <build><plugins> -->
io.github.rerorets
hadome-maven-plugin
0.4.1
Run it with mvn hadome:check.
Rust
Anything else (Terraform, Ansible and others)
You can also install from an archive on the releases page. On Windows, which has no Homebrew, use this or the Python way. The steps below put hadome on PATH. On Windows, call it from a terminal opened after PATH is changed.
# Windows (PowerShell); on an arm64 machine, use windows_arm64
Invoke-WebRequest https://github.com/rerorets/hadome/releases/latest/download/hadome_0.4.1_windows_amd64.zip -OutFile hadome.zip
Expand-Archive hadome.zip -DestinationPath "$HOME\hadome"
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$HOME\hadome", "User")
# open a new terminal, then:
hadome version
# macOS and Linux; linux_arm64, darwin_amd64 and darwin_arm64 are there too
&&
2. Tell your AI agents
This writes a block on how to use hadome into the file your agents read. The path is relative to the current directory: run it anywhere but the root and you get a separate CLAUDE.md there.
- Any other file, such as
AGENTS.md, works too. List several, as inhadome agents --write AGENTS.md CLAUDE.md, to write into each. - The file is created if it is missing. Later runs update the same block.
- Add
--lang jafor a Japanese block. - The details are in the guide for agents.
To add it by hand, paste this block as it is, including the first and last lines, <!-- hadome:begin --> and <!-- hadome:end -->. With those two lines, hadome agents --write can later rewrite the same block.
This repository uses hadome to stop regressions in code and tests.
- -------
Example output
A change that deletes the only test of store.Append, adds a sleep to another test, and adds an import the dependency diagram does not draw:
$ hadome check --base main
✗ deps/not-in-diagram import not in the diagram: store → core (no arrow store --> core)
store/store.go:3
why: the diagram (docs/deps.md) is how people and agents know what may depend on what; an import it does not draw makes it wrong
fix: depend on what the diagram allows, often through an interface the importing unit declares; if the design changed, draw the arrow, which a person reviews
✗ tests/no-sleep test waits by sleeping: time.Sleep(10 * time.Millisecond)
worker/worker_test.go:11
why: a sleep is either too long (a slow suite) or too short (a flaky test); it never waits for the actual event
fix: wait for the event itself: a channel, a callback, or a condition with a deadline that fails loudly
✗ tests/scope-shrunk guarded scope shrank: example.com/app/store.Append is no longer exercised by any active test (was: TestAppendKeepsRows)
store/store_test.go:9
why: the behaviour of example.com/app/store.Append can now change without any test noticing
fix: keep or restore a test that calls example.com/app/store.Append, or remove example.com/app/store.Append itself if it is no longer needed
3 regression(s). 0 existing finding(s) unchanged (not blocking).
The output is in English by default. With --lang ja it comes out in Japanese: the findings' wording (message, why, fix), hadome explain <rule> and the summary line. JSON keys, rule IDs and exit codes do not change. The setting that makes it Japanese without the flag is under "Configuration and exceptions" below.
What it checks
| Group | Rules |
|---|---|
tests/ |
scope-shrunk (what tests guard shrank) · invariant-unguarded · no-clock · no-sleep · always-true · skip-without-reason · no-mock (mocks in tests of pure code) · guards-nothing (tests that call no product code) · no-assertion (tests without an assertion) · duplicate (tests with the same body) · purity-lost and target-unknown (review) |
deps/ |
not-in-diagram (imports your dependency diagram does not draw) · diagram-stale (boxes that name nothing) |
size/ |
file-lines · function-lines · params · complexity · interface-methods |
comments/ |
todo-format (TODOs without a when) · missing-doc · broken-reference (comments pointing at missing documents) |
unknown/ |
unread (review): a file hadome could not read · import (review): an import whose destination is decided only at run time |
mutation/ |
survived (hadome mutate only): a break no test notices |
config/ |
loosened (review): a change that loosens the rules judging it |
Every rule reports why it matters and how to fix it. The rules are specified in docs/design.
Regression vs. cleanup
When three tests disappear, their number alone cannot tell cleanup from regression. hadome works out the functions and methods each test calls (its targets), and compares the set of targets that active tests reach: the guarded scope.
| What you did | Count | Guarded scope | Verdict |
|---|---|---|---|
| Merged three duplicate tests into one | Down | Unchanged | Cleanup |
| Deleted code together with its tests | Down | Gone with the targets | Cleanup |
| Deleted or weakened only the tests | Down or same | Narrower | Regression |
A behaviour you no longer need passes as cleanup when you delete the code along with its tests.
The background, thinking of tests in three layers (invariants, writing rules, gate), is in the model.
Where it runs and exit codes
| Where | What to run | What you get |
|---|---|---|
| Inside an agent's loop | hadome check --format json |
For each regression: the rule, file and line, why it matters, how to fix it |
| Before a push | hadome check --head HEAD (as a pre-push hook) |
A readable list |
| CI on every pull request | hadome check --base origin/main |
An exit code and a summary to post as a comment |
| Before finishing a change (locally) | hadome mutate |
The breaks no test noticed |
| Exit code | Meaning |
|---|---|
0 |
No regressions |
1 |
One or more regressions |
2 |
Configuration error. The check cannot be trusted, so it does not pass |
3 |
No regressions, but something needs a person's review (a new exception, a configuration that loosens the rules, and so on) |
hadome only reads the repository and talks to no service such as GitHub. To post the summary as a comment, do it from CI. A GitHub Actions example is in Using hadome in CI.
Configuration and exceptions
Settings go in .hadome.yml: limits, the dependency diagram, invariants and so on. hadome works without the file.
hadome init writes a minimal .hadome.yml; it never overwrites one that exists. To have the output in Japanese every time, add messages: ja to it. --lang, when given, comes first.
Changing the settings and proposing invariants is the agents' job. Agents read the specification with hadome explain <key> (the guide for agents). People decide which proposals to accept and whether to allow a change that loosens the rules. Such a change stops with exit code 3 for a person's review.
Writing an exception
Write it where it applies, with a reason. Without a reason it has no effect.
// gate:allow tests/no-sleep reason: the OS file watcher offers no event to wait on
time.Sleep(50 * time.Millisecond)
Rules only your product needs
A rule such as "never UPDATE the log store" is a small program in your own repository, in any language. hadome treats it like the built-in rules: it reports only regressions, and reasoned exceptions work for it too. How to write one is in product-specific gates.
Trying mutations
With mutation in .hadome.yml, hadome mutate breaks the functions a change touched, one place at a time, and tries whether the tests notice. A break no test notices is reported as a mutation/survived regression.
mutation:
test:
It runs the tests once per break, so it takes far longer than check. It is meant to run locally, before a change is finished. The guide for agents has the details.
The name
歯止め (hadome) is the Japanese word for a pawl: the part of a ratchet that lets the wheel turn one way and stops it from turning back. hadome likewise stops only changes that make things worse than main.
Versions and bug reports
While hadome is 0.x, behaviour may change between versions (versioning). Bugs and requests go to Issues.