Define standards once. Let machines enforce them. Debug with confidence.
AGENTS.md as the source of truthAI coding agents are powerful, but unpredictable.
Result: production code that is hard to review, maintain, and trust β whether it was written by a human or an agent.
A guardrail makes the right thing easy and the wrong thing fail loudly β automatically, every time.
Two complementary layers:
| Layer | Goal | Mechanism |
|---|---|---|
| Context | Tell the agent the rules up front | AGENTS.md, conventions, examples |
| Enforcement | Catch what slips through | Static analysis, hooks, CI, runtime |
Context reduces mistakes. Enforcement guarantees they never merge.
“Curate what the AI sees so it has to guess less.”
Key insight: AGENTS.md is the agent-agnostic place to write this down once β readable by humans and by any AI tool.
π https://agents.md/
The context window, top to bottom:
System instructions β vendor/tool base behavior
Custom instructions β your AGENTS.md, team conventions
Conversation history β prompts, replies, corrections
Implicit context β open files, selection, git diff
Explicit references β #file, pasted snippets
Tool outputs β build / test / lint feedback
You control the middle layers and the tool outputs. You don’t control model reasoning or perfectly repeatable output β so make the controllable parts strong.
AGENTS.md β structureA plain Markdown contract that lives in the repo:
# Project Overview
## Build and Test Commands # make build / make test / mvn verify
## Code Quality and Style # JavaDoc on public methods, no NPEs
## Architecture # constructor injection, immutable DTOs
## Security # no PII in logs, validate all input
## Plugins # Checkstyle / SpotBugs / ArchUnit configs
Keep it at the workspace root β the single source of truth that
CLAUDE.md and other assistant docs inherit from.
AGENTS.md β discovery: nearest winsproject-root/
|-- AGENTS.md β 1. read first (global rules)
+-- src/main/
|-- AGENTS.md β 2. read next (module rules)
+-- java/com/app/service/
|-- AGENTS.md β 3. read last (most specific β overrides parent)
+-- UserService.java β file being generated
Rules merge top-down; the nearest
AGENTS.mdwins on conflict. Put broad rules at the root, exceptions close to the code.
AGENTS.mddefines the standards. Static tools enforce them.
The gap if you stop at documentation:
The fix: automated, measurable, blocking checks β so review can focus on functional logic, not brace placement.
| Tool | Catches | Config |
|---|---|---|
| Checkstyle | style, naming, formatting, line length | checkstyle.xml |
| SpotBugs | NPEs, resource leaks, bad casts, overflow, concurrency | spotbugs-exclude.xml |
| ArchUnit | layering, cycles, forbidden APIs, conventions | *ArchitectureTest.java |
All three run inside one command:
mvn verify # or: make coverage
If any gate fails, the build stops. No green build β no merge.
Without it: inconsistent naming, random indentation, unreadable diffs.
With it:
make format auto-fixes most violationsStyle stops being a code-review opinion and becomes a build result.
Bytecode-level analysis that catches what compiles but breaks:
equals/hashCode mistakes“It compiles” β “it’s correct.” SpotBugs closes part of that gap for free.
Without it: circular deps, layering violations, business logic in the wrong layer, the same anti-pattern copy-pasted everywhere.
With it β architecture rules become JUnit tests:
A bytecode analysis engine + a fluent assertion DSL. Pure static
analysis on compiled .class files β no Spring context, no running app.
.class files
β 1. Import β ClassFileImporter builds a graph of
JavaClass / JavaMethod / JavaField
β 2. Evaluate β fluent DSL walks that object graph
β 3. Condition β each match checked by an ArchCondition
β 4. Report β failures listed with FQN + line + reason
Because it reads bytecode directly, it can see annotations, inheritance, field types, and even method-call relationships without executing code.
The three-part DSL β what().that(predicate).should(condition):
@AnalyzeClasses(packages = "com.example",
importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {
@ArchTest
static final ArchRule noFieldInjection =
noFields().should().beAnnotatedWith(Autowired.class)
.because("use constructor injection");
}
| β Can check | β Cannot check |
|---|---|
| annotations, inheritance, interfaces | runtime behavior / return values |
| method-call & package dependencies | dynamic-proxy behavior |
| field types, method signatures | application.yml config |
| custom bytecode patterns | reflection targets |
Common “ArchUnit red flags” β CI fails if present:
noFields().should().beAnnotatedWith(Autowired.class); // no field injection
noClasses().should().callConstructor(ObjectMapper.class);// reuse shared bean
noClasses().should().callConstructor(RestTemplate.class);// use RestClient
noClasses().should().accessClassesThat()
.haveFullyQualifiedName("java.lang.System"); // no System.out
classes().that().areAnnotatedWith(RestController.class)
.should().haveSimpleNameEndingWith("Controller");
The team agreement and the build check are the same artifact. Custom
ArchCondition<T>/DescribedPredicate<T>handle anything bespoke.
+-----------------------------+
| Coding Agent (any tool) |
| reads AGENTS.md, writes |
+--------------+--------------+
| generates code
v
+-----------------------------+
| mvn verify |
| Checkstyle . SpotBugs . |
| ArchUnit . tests |
+------+---------------+------+
| |
violations all pass
| |
v v
feedback to agent ready for human
(report + AGENTS.md) functional review
|
+--> agent fixes -> re-runs verify -> loops until green
The tool output is the prompt for the next iteration.
CI is the last line of defense, not the first. Catch violations before
they leave the laptop, using Git hooks wired through a Makefile.
Self-installing β the first make sets it up:
_HOOKS_PATH := $(shell git config --get core.hooksPath 2>/dev/null)
ifneq ($(_HOOKS_PATH),.githooks)
_ := $(shell test -d .git && git config core.hooksPath .githooks \
&& chmod +x .githooks/* 2>/dev/null)
endif
Hooks live in
.githooks/(version-controlled), not the un-tracked.git/hooks/. Everyone gets the same gates with zero setup.
# pre-commit β fast feedback
mvn -q test # compile + unit tests
mvn -q spotless:check # is it formatted? (make format to fix)
# + if a DB changelog is staged β check it applies cleanly on a fresh DB
# commit-msg β enforce Conventional Commits
^(feat|fix|docs|refactor|perf|test|build|ci|chore|revert)(\(scope\))?!?: ...
# pre-push β the heavier gate before sharing
mvn -q spotbugs:check
mvn -q verify # integration tests + coverage + ArchUnit
Escape hatch for emergencies: git push --no-verify.
One vocabulary for humans and agents β AGENTS.md points here:
make build # mvn clean package -DskipTests
make test # unit tests
make coverage # mvn verify + jacoco report
make format # spotless:apply (auto-fix style)
make spotbugs # spotbugs:check
make liquibase/check # fresh container β update β validate β teardown
Discoverable, repeatable commands beat tribal knowledge β and an agent can read the
Makefileto learn how to build and test the repo itself.
Static analysis stops bad code. The cluster stops bad behavior:
kubeconform / schema checks in CISame philosophy as ArchUnit: encode the rule once, fail loudly when it’s broken.
When a guardrail trips, investigate fast:
kubectl get pods -o wide # status, restarts, node
kubectl describe pod <pod> # events: OOMKilled, ImagePullBackOffβ¦
kubectl logs <pod> -c <container> -f # stream logs
kubectl logs <pod> --previous # logs from the crashed container
kubectl exec -it <pod> -- sh # shell inside a running pod
kubectl port-forward <pod> 8080:8080 # hit the service locally
kubectl debug <pod> --image=busybox \ # ephemeral container for
--target=<container> # distroless images
Read the events first β
CrashLoopBackOff,OOMKilled, andImagePullBackOffeach point at a different fix.
Don’t rebuild-push-redeploy by hand on every change:
Faster feedback at every layer β IDE, build, hook, CI, cluster β is the whole point of guardrails.
IDE / Agent AGENTS.md curate context (Layer 1)
|
git commit pre-commit + commit-msg format, compile, unit test, message
|
git push pre-push: spotbugs + verify (IT) + spec-check
|
CI Checkstyle . SpotBugs . ArchUnit . test . JaCoCo (blocking)
|
Kubernetes probes . limits . admission policies runtime
|
Incident kubectl events / logs / debug observe
Each layer is cheap, fails fast, and catches what the previous one missed.
AGENTS.md is agent-agnostic and human-readableTry it on one repo this week:
AGENTS.md at the rootmvn verify.githooks/ through your MakefileQuestions & discussion welcome.
π References: agents.md Β· ArchUnit docs Β· the project’s Makefile & .githooks/