Spring Boot team harness
The harness steers the agent in three places. AGENTS.md tells it how this repository works before it starts. Hooks check its work before a turn ends, so it fixes its own mistakes. ./mvnw verify in CI holds the same gates for every developer and every tool. Rules live in the build wherever they can: a gate holds whoever writes the code and costs no context.
Apply it
-
Run the script from the project root. Preview with
--dry-runfirst:bashpython3 ${CLAUDE_SKILL_DIR}/scripts/apply_harness.py . --dry-run python3 ${CLAUDE_SKILL_DIR}/scripts/apply_harness.py .${CLAUDE_SKILL_DIR}is this skill's folder. Codex doesn't set it, so there use the folder thisSKILL.mdis in. Run it in the Maven project's folder. When that folder is inside a larger repository (for examplebackend/next tofrontend/), the CI workflow,.claude/settings.json, the hooks and.gitattributesgo to the repository root, where GitHub and Claude Code read them, and the rootAGENTS.mdgets a line pointing to the project's own. The Stop hook then reacts only to changes in that folder.The observability pack comes by default: dependencies, safe defaults, a local Grafana stack, Service health and JVM dashboards, a filter that logs and marks every handled 5xx, a test check that fails on logged emails and credentials, and
docs/observability.md. Telemetry stays off in deployments untilOTLP_EXPORT_ENABLED=true. It narrowsmanagement.endpoints.web.exposure.includeto what's safe on the public port, and says so. Pass--no-observabilityto skip it.Pass
--no-moduleswhen the base package's subpackages are layers (controller,service,domain) rather than modules; Spring Modulith would report them as cycles. Rerunning with--no-modulesafter a run without it removesModuleBoundariesTestsand theadd-moduleskill unless they were edited, and names what to remove fromAGENTS.mdby hand.NullAway checks every class under the base package by default. On an existing codebase with many null findings, offer
--nullaway marked: NullAway then checks only packages whosepackage-info.javahas@NullMarked, so the team adopts null-safety one package at a time instead of marking classes@NullUnmarkedacross the codebase. The script reports how many packages are marked. With none, NullAway checks nothing, so mark at least the packages the team changes most. A rerun keeps the pom's mode unless--nullawayis passed again. Switch to--nullaway allonce every package is marked.It adds only what the project lacks. In the pom, it skips any property, dependency or plugin that is already declared, so an existing Spotless or Spring Modulith setup stays as it is. It adds
CLAUDE.md(one line,@AGENTS.md),.claude/settings.jsonand its two hook scripts,.mvn/jvm.config, the three tests, a CI workflow, the CI integrity check, and project skills in both.claude/skills/and.agents/skills/. It merges its permissions, hooks and plugin into an existing.claude/settings.json, adds its CI workflow only when no workflow runs Maven (asharness-verify.ymlifverify.ymlis taken), appends its gate section to an existingAGENTS.md, skips the boundary test when a test already callsApplicationModules.of(...), and never overwrites a file the team has edited. Rerun it after updating this plugin: it adds the pieces the project is missing and updates the files it owns (hooks, tests, project skills, its workflow) that nobody has changed since, which it tracks in.claude/harness.lock. It reports each edited one so you can merge the new version by hand. Don't copy harness files into a project by hand: rerun the script instead. A rerun also fills in template placeholders left inAGENTS.md, and reports anarchunit.propertiesthat allows store creation. If<build><plugins>already configuresmaven-compiler-plugin, it stops: merge that part ofassets/pom/plugins.xmlby hand and run it again. -
Run
./mvnw spotless:applyand commit the result on its own, so the reformat stays out of later diffs. On an existing codebase, add that commit's hash to.git-blame-ignore-revssogit blameskips it. -
Get
./mvnw verifygreen. New code must meet the gates; code that already fails them is recorded, not rewritten:- If there are only a few findings, fix them with the user. Each NullAway finding is a question about what a missing value means, so don't guess.
- Otherwise run
python3 ${CLAUDE_SKILL_DIR}/scripts/baseline.py .. It marks each class NullAway rejects today@NullUnmarked, and records today'sCodeRulesTestsviolations insrc/test/resources/archunit_store. New code is checked in full, and a fixed violation drops out of the record. Remove@NullUnmarkedfrom a class once it's fixed. When an update of the harness adds rules, runbaseline.pyagain: a rule the record doesn't list fails on existing code too, and the rerun records only the new rules' violations. Don't suppress warnings or skip tests to get green. The Stop hook only blocks on problems in files the agent changed, but untilverifyis green it can't fully check those either.
-
Draft the
Modules,Run and testandTeam conventionssections ofAGENTS.mdfrom the code, then go through them with the user. Read the packages, the pom, compose files, the test setup, and repeated patterns such as error handling, DTO mapping and naming. Write only what an agent would get wrong without being told: one line per module, the commands and services the app needs, and the conventions the code follows. Mark each convention you inferred(inferred); the user confirms or deletes it. Leave out generic Spring advice and anything the code makes obvious, and keep the file short: every session reads all of it. -
Commit everything, including
.claude/settings.json, the marked classes andarchunit_store. -
On an existing codebase, find the files where comments make up a large share of the lines, often DTOs and records. Agents write comments as densely as the code they read, so the comment rule in
AGENTS.mdholds only once those files follow it. Offer to prune them module by module, each module in its own pull request, withcode-reviewer's comment pass: it changes only comments. -
Offer a project verification skill, which proves behaviour the gates can't see. Read the repository and recommend one generator from pstack. Only the user can start them:
- The app has a UI, or signs users in with
oauth2Login:/pstack:create-verification-skill. It generates a skill that drives the real app and saves evidence. For a browser app, tell it to followreferences/browser-verification.md. - The riskiest code is a state machine, retries, idempotency or concurrent updates:
/pstack:create-formal-verification-skill. It checks one property of that code against every case in a stated scope. - Both apply: start with the first; the second can follow in its own pull request.
If the project has
docs/observability.md, tell the generator to read it, so journeys also check for server errors the page hides. If pstack isn't installed, say so and skip this step. Adding the skill toAGENTS.mddoesn't make agents use it. They verify when the task asks for proof, so tell the team to ask for it in the task.
- The app has a UI, or signs users in with
What each piece does
| File | Effect |
|---|---|
AGENTS.md | Instructions both Claude Code and Codex read. Says that done means ./mvnw verify passed, and how to fix each gate |
CLAUDE.md | Imports AGENTS.md for Claude Code versions that don't read it directly |
REVIEW.md | At the repository root, for reviewers: the failures the code doesn't show, which changes are high risk, what not to report, and how to report. The code-reviewer skill and Claude Code's GitHub review read it. Added only when missing; the team fills in the failures, one line each time a review misses a bug |
.claude/settings.json | Lets ./mvnw run without prompts, blocks reading .env files, enables this plugin for every teammate with this marketplace set to update itself, and registers the hooks. For the other skills the team uses, each developer runs install.py from this marketplace once |
.claude/hooks/verify-on-stop.sh | SessionStart and Stop hook. It records the code each session starts from. When a turn ends and the session has changed the code under src or pom.xml, committed or not, it formats the code and runs the gates that need no Docker (compile with NullAway, CodeRulesTests, ConfigurationKeysTests, BuildSetupTests, the module and ArchUnit tests). It blocks the turn only on errors in files the session changed, so Claude fixes its own mistakes and never code it didn't touch; when the build fails only elsewhere, it tells the user the change couldn't be fully verified. A formatting failure blocks too. It skips code that already passed, and gives up after one failed retry on the same code. A session that changes no code costs nothing; a check takes 25 to 75 s |
.claude/hooks/guard.sh | PreToolUse hook. Refuses git push --force, git reset --hard, git clean -f, edits to a Flyway migration that is already on the main branch, edits to the ArchUnit freeze record, and edits to the rule tests (CodeRulesTests, ConfigurationKeysTests, BuildSetupTests, ModuleBoundariesTests, LogLeakCheck, and progen's ArchUnitTests and ModularityTests). A developer changes a rule by hand. It also refuses Bash commands that write to those files and the Maven flags that record violations as accepted (allowStoreCreation=true, freeze.refreeze=true). It is advisory, not a boundary: a command it doesn't recognise gets through, so harness-integrity.yml backs it up in CI |
scripts/report.py | Summarises what the hooks did in this clone: turns checked, passed and blocked, blocks by gate, how many the agent fixed, check times, and what the guard refused. Both hooks append one line per run to .git/harness/events.jsonl (no paths, commands or file contents; capped at 2000 lines). Run python3 ${CLAUDE_SKILL_DIR}/scripts/report.py . when the team asks whether the hooks are worth their time |
scripts/baseline.py | For an existing codebase: marks the classes NullAway rejects today @NullUnmarked and records today's CodeRulesTests violations, so verify passes while new code is held to the gates |
ConfigurationKeysTests | Fails on spring., management., server. and logging. keys that no dependency defines, or that are deprecated. Boot ignores those keys silently |
BuildSetupTests | Fails on shortcuts that pass the tests while production runs without the feature: a Boot starter or module in test scope, spring-boot-autoconfigure-classic, Flyway migrations without spring-boot-starter-flyway (Boot 4 doesn't run them with flyway-core alone), unless the app builds Flyway itself, and, when the project has migrations, ddl-auto set to create or update the schema or code that writes flyway_schema_history |
UnusedCodeTests | Fails on a non-private method that only tests call: dead code its tests keep alive, or an internal opened up so a test can look inside. Getters, Spring entry points, overrides and methods named in @PreAuthorize expressions are left out. It runs in verify but not in the Stop hook, because a service is often written and tested before anything calls it |
ModuleBoundariesTests | Spring Modulith verify(): no use of another module's nested packages, no cycles |
CodeRulesTests | ArchUnit rules for mistakes that compile and pass most tests: a class calling its own @Transactional, @Async, @Cacheable or @Retryable method (the proxy is skipped), controllers that start transactions, use repositories, or take or return JPA entities, @Retryable or @ConcurrencyLimit with nothing switching them on, @ManyToMany fields that aren't a Set (with a List, removing one element rewrites every join row), @ManyToOne and @OneToOne without fetch = LAZY, enum columns stored by position, a @Transactional method calling an HTTP client, an HTTP interface or an AWS SDK client, directly or through the project's own classes (it holds a database connection for the whole call), RestClient.create(), WebClient.create(), new RestTemplate(), or the static builder() in a class that sets no request factory (no timeouts), proxy annotations on private, final or static methods, @Async or @Scheduled with nothing switching them on, services that throw ResponseStatusException, use the servlet API or return ResponseEntity, @Deprecated code outside controllers (update the callers and delete it), @Autowired fields, System.out, Jackson 2 (Boot 4 ignores it), Spring's deprecated nullness annotations, JUnit 4 tests (never run), and @Disabled tests. Each failure message says how to fix it. It uses the project's ArchUnit or Taikai, and adds ArchUnit only when neither is declared |
.github/workflows/harness-integrity.yml, .github/harness/check-protected.sh | Fails a pull request that adds entries to the ArchUnit freeze record, changes archunit.properties, changes or deletes an existing rule test, changes or deletes a migration on the base branch, changes the hooks or this check, or stops LogLeakCheck running by unregistering it or turning off extension autodetection, whichever tool made the change. Removing fixed violations from the record and adding new migrations or rule tests pass, and so do other edits to those two JUnit files, which aren't checked at all in a project without LogLeakCheck. It runs the base branch's copy of the script, so a pull request can't weaken it. A maintainer accepts an intended change, such as a harness update, with the harness-rule-change label; the files are still listed for the reviewer. Make it a required status check and put .github/ and the rule tests under CODEOWNERS. Added for every project, also when CI already runs Maven |
.github/workflows/verify.yml | Runs ./mvnw verify on every pull request, so the gates hold for code nobody ran locally. Added only when no workflow already runs mvnw |
| pom gates | NullAway in JSpecify mode on main code at compile time (tests are excluded, since they routinely dereference response bodies), Spotless check |
| Project skills | add-module (module layout and test, for module-based projects) and db-migration (Flyway naming and safe changes, when the pom uses Flyway). Each tool sees only their names and descriptions until one is used |
Limits
- Maven only. A Gradle build needs the same gates written as Gradle plugins.
- Error Prone needs JDK 21.0.8 or later to build. Projects that set up the compiler plugin before the
-XDaddTypeAnnotationsToSymbol=trueargument was added need it added by hand to build on JDK 21. - The hooks are Claude Code only. Codex sessions get
AGENTS.md, the project skills and the CI gates, andAGENTS.mdtells them to run./mvnw verifythemselves. - The hooks are bash scripts. On Windows, Claude Code runs them with Git for Windows; without it they fail.
- Tool versions are pinned in the pom properties. Update them together and rerun
./mvnw verify.
MCP servers
Add none by default. Each server's tool definitions take context in every session, and the gates above need none. If the team already runs SonarQube, its MCP server (sonarsource/sonarqube-mcp) can show issues and quality gates. It needs a SonarQube Cloud or Server token and Docker. Limit it with SONARQUBE_TOOLSETS=issues,quality-gates and SONARQUBE_READ_ONLY=true, and keep the token in each developer's environment, never in the repository.


