Follow this repo's Maven rules — versions only in the root pom, module poms version-free, the build profiles, and clean-after-codegen. Use when editing a pom.xml, adding a dependency or plugin, choosing a build command, or when the user says "add a dependency", "bump a version", or "the build fails".
Scanned 8/31/2026
Install to Claude Code
npx -y skills add adamw7/tools --skill maven-conventions --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Maven Conventions?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/adamw7-maven-conventions)More formats (shields.io, HTML) on the badges page.
---
name: maven-conventions
description: Follow this repo's Maven rules — versions only in the root pom, module poms version-free, the build profiles, and clean-after-codegen. Use when editing a pom.xml, adding a dependency or plugin, choosing a build command, or when the user says "add a dependency", "bump a version", or "the build fails".
---
# Maven Conventions Skill
Keep `pom.xml` edits and build commands consistent with how the `tools`
multi-module reactor is wired. Getting versions in the wrong place, or missing a
profile, is the most common source of avoidable build friction here.
## Hard rules
### Versions live in exactly one place
- **Dependency versions and scopes**: only in the **root** `pom.xml` under
`<dependencyManagement>`. A module may say one thing about a managed
dependency — a *narrower* scope, where two modules genuinely want different
ones (`enforcer-api` is managed `provided` and narrowed to `test` by `adopt`;
`jsoup` is managed unscoped and narrowed to `test` by `claude-code-enforcer`).
- **Plugin versions**: only in the **root** `pom.xml` under
`<pluginManagement>`.
- **Module poms reference dependencies and plugins WITHOUT versions.** Never add
a `<version>` to a module pom — add or change it in the root instead. The
exceptions are reactor artifacts, which use `${project.version}`.
- **Artifacts that must move together share one property**, not a version each:
`protobuf.version` (runtime *and* `protoc`), `grpc.version`, `derby.version`,
`log4j2.version`, `maven.api.version`. `derby.version` and `log4j2.version`
are Spring Boot's own property names, so overriding them also moves the
siblings the `spring-boot-dependencies` BOM manages (derbyshared, the other
log4j2 artifacts) instead of leaving them on Boot's older version.
- **The Spring Boot parent manages plugins too.** It binds a `generate`
execution on `protobuf-maven-plugin` and adds `protoc-gen-grpc-java` to it for
its gRPC starter; the root pom unbinds that execution (`<phase>none</phase>`)
and clears the generator list (`<plugins combine.self="override"/>`), because
the modules here declare the protobuf executions they want. Leave both in
place — without them `protogen-maven-plugin` fails at `generate-sources`
looking for a `src/main/proto` it does not have.
### Ask before adding a dependency
- Use the existing Maven dependencies. **Always ask the user before adding a new
one.** If approved, declare it (with version) in root `<dependencyManagement>`,
then reference it version-free in the module.
### Name the module of every published jar
- A published jar states its JPMS module name instead of letting the JVM derive
one from the filename, which changes with the artifactId or the version scheme
and breaks a consumer's `requires`.
- `data` and `data-test` state it in `module-info.java`. Every other published
module states it in the jar manifest, via `maven-jar-plugin`:
```xml
<archive>
<manifestEntries>
<Automatic-Module-Name>tools.markdown.common</Automatic-Module-Name>
</manifestEntries>
</archive>
```
- The name is the artifactId with every non-alphanumeric run collapsed to a dot
(`tools.markdown-common` → `tools.markdown.common`), so pinning it changes
nothing for a consumer already reading the derived name.
`protogen-maven-plugin` is the exception: no `tools.` prefix in its artifactId,
and nothing `requires` a Maven plugin, so it takes `tools.protogen.maven.plugin`.
- **A new published module needs this too.** The unpublished ones
(`grpc-example`, `assembly`, `*-test`, anything with
`central.skipPublishing`) need nothing.
### Clean after removing a code-generation source
- Run `clean` after deleting a codegen input, so stale generated builders in
`target/` cannot mask the change.
## Build commands (run from the repo root)
| Command | Purpose |
|---|---|
| `mvn clean install` | Full clean build + install to local repo |
| `mvn install` | Faster incremental build |
| `mvn -pl <module> -am test` | Tests for a single module (`-am` is required — a bare `-pl` fails the `ReactorModuleConvergence` rule) |
| `mvn -P integration-tests verify` | MCP integration tests (`*IT`) |
| `mvn -Pcoverage verify` | JaCoCo coverage (fails under 80% instruction or branch) |
| `mvn -Ppitest test` | PIT mutation testing |
| `mvn install -Dskip.shellcheck=true` | Skip the shellcheck lint |
## Notes that save time
- **Shellcheck runs embedded.** `dev.dimlight:shellcheck-maven-plugin` uses
`<binaryResolutionMethod>embedded</binaryResolutionMethod>` — the binary ships
in the plugin jar (from Maven Central), so the build needs no installed
`shellcheck` and works offline. Skip it with `-Dskip.shellcheck=true` when you
just want a fast loop.
- **Java 25** is required: JDK 25 on `PATH` with `JAVA_HOME` set.
- The root reactor modules are: `markdown-common`, `claude-code-enforcer`,
`test-common`, `mcp-common`, `data`, `code`, `adopt`, `grpc-example`,
`assembly`. `data-test` is built separately (not in the root `<modules>`).
- **`markdown-common` takes no dependencies.** It is shared by the enforcer rule
and by `adopt` precisely so both read a document identically; anything added
there travels to every consumer of either, including repositories that resolve
the enforcer rule as a maven-enforcer-plugin dependency. Its architecture test
fails the build on a dependency outside `java..`.
- **A typo in `-P` fails the build**, not the run: the root `enforce` execution
runs `requireProfileIdsExist`.
- **The `claude-md-enforce` profile activates on the root project only**, by
requiring a `CLAUDE.md` beside the pom as well as the `enforceClaudeMd`
property. A profile in a parent is inherited, so without that second condition
every module takes on the profile's `maven-enforcer-plugin` declaration — and
Maven orders a project after every plugin dependency it declares, giving each
module a reactor edge to `claude-code-enforcer`. Maven quietly tolerates a
cycle closed by a plugin edge, so that stayed invisible until a reactor module
became a real dependency of the enforcer (`markdown-common`); the cycle then
closed through a dependency edge and `mvn -B package -DenforceClaudeMd` could
not sort the reactor at all. **Do not put a reactor artifact in a plugin's
`<dependencies>` in an inherited profile.**
- **A module that is not a reusable library** (example, test harness,
distribution) opts out of publishing with `<maven.deploy.skip>` and
`<central.skipPublishing>` in its `<properties>` — never by redeclaring the
`release` profile.
## Workflow for a dependency/plugin change
1. Confirm it isn't already managed in the root pom.
2. If it's a *new* dependency, **ask the user first**.
3. Add/adjust the version in root `<dependencyManagement>` or
`<pluginManagement>`.
4. Reference it version-free in the module pom.
5. Build: `mvn install` (add `clean` if you removed a codegen source).
## References
- `CLAUDE.md` / `AGENTS.md` — *Maven* section (source of truth)
- Root `pom.xml` — `<dependencyManagement>`, `<pluginManagement>`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!