What users install from maddi: the command-line distributions, the Gradle and Maven plugins, and the IDE plugins with the analysis daemon behind them. maddi's README explains what maddi computes; this one explains how to run it.
The analyzer needs JDK 25 or later to run. The Kotlin distribution needs JDK 26 or later.
Two self-contained zips on the GitHub release. Unpack and
run: every jar is in lib/, and the launcher carries the JVM flags the analyzer needs.
| Zip | Launcher | Analyses |
|---|---|---|
maddi-<version>.zip (maddi-cli) |
bin/maddi |
Java |
maddi-kotlin-<version>.zip (maddi-cli-kotlin) |
bin/maddi-kotlin |
Java and Kotlin |
bin/maddi-kotlin takes the same command line as bin/maddi and does strictly more. On a project with no .kt
file, it runs the Java analyzer and behaves identically. Take the small zip for a Java-only build: the Kotlin one
carries about 85 MB of K2 compiler jars. Kotlin support ships only this way, because it depends on JetBrains
artifacts that are not on Maven Central.
An option the mixed Java/Kotlin pipeline cannot honour yet is refused by name, with exit code 7, never run as a
no-op. Writing analysis results, incremental analysis and the hints compiler are among those options. --translate
is one too: a project with Kotlin sources has no single Java program to translate. --skip-kotlin-sources analyses
only the Java part of a mixed project, and accepts an incomplete result.
To point the CLI at a project, capture what the build compiled and hand it over. No build-tool integration is needed:
./gradlew :your-module:compileJava --debug 2>&1 | grep 'Compiler arguments:' > build.log
maddi --compile-log build.log --analysis-steps modification --analysis-results-dir out--compile-log also reads Maven and Ant logs. The analysis loads the JDK and library results that ship with maddi;
--preload-analysis-results-dirs none turns that off.
The same command line translates the project's main sources:
maddi --compile-log build.log --translate kotlin --translate-output kotlin-src
maddi --compile-log build.log --translate csharp --translate-output csharp-src # experimental; with a Translated.csprojThe modification analysis and the nullability pass run first, and their verdicts decide T or T?, where !! (or
!) is needed, and which collections are read-only. --analysis-steps none translates without them.
translation-report.txt in the output lists the printer's crashes and messages; a crash leaves that one file out and
exits non-zero.
io.codelaser.maddi.analyzer on the Gradle Plugin
Portal:
plugins {
java
id("io.codelaser.maddi.analyzer") version "0.9.5"
}
maddi {
analysisSteps = "modification" // parse -> prep -> modification -> write results
}./gradlew maddi-analyzer # one result .json per package, under build/maddiThe plugin takes the source sets, the classpath, the language level and --enable-preview from the Gradle model,
and forks a worker JVM with the flags the analyzer needs. Its options are in
maddi-gradleplugin/README.md.
io.codelaser:maddi-mvnplugin on Maven Central, goal prefix maddi, five goals: run,
write-input-configuration, statistics, write-analysis-hints and compile-analysis-hints. The JVM that runs
Maven needs javac's --add-exports flags for the analysis goals. Set them once in .mvn/jvm.config or
MAVEN_OPTS; the user manual lists them.
The IntelliJ, Eclipse and VS Code plugins are not published yet. Each shows maddi's computed annotations in the
editor. All three talk to the same analysis daemon (maddi-ide-daemon), which runs out of process, so the IDE can stay on any
JVM.
| IDE | Module | Build | Notes |
|---|---|---|---|
| IntelliJ IDEA | maddi-intellij |
./gradlew :maddi-intellij:buildPlugin, or runIde to try it |
Java and Kotlin. The Kotlin front end is downloaded on demand from the release and checked against its SHA-256. |
| Eclipse 2025-09+ | maddi-eclipse |
task eclipse:build (a p2 update site) |
maddi-eclipse/README.md |
| VS Code | maddi-vscode |
task vscode:package (a .vsix) |
Needs redhat.java. maddi-vscode/README.md |
task --list shows the other tasks, and task doctor checks the toolchain.
maddi-intellij-j2k is not a plugin. It is a headless starter that runs IntelliJ's own J2K converter on a corpus, so
that maddi's Kotlin translation can be compared with it.
maddi-mcp is an MCP server on stdin/stdout, for a coding agent to configure a
project, analyse all or part of it, and ask for the verdicts. It drives the same daemon as the IDE plugins, bundled
in its daemon/ directory. It runs on a JDK 25 or later, as the daemon does: by default the daemon uses the same one,
or the one --jdk names. On macOS and Linux, brew install codelaser/tap/maddi-mcp installs it with
its JDK (MADDI_JDK_HOME names another, Temurin for example), and claude mcp add maddi -- $(brew --prefix)/bin/maddi-mcp registers it with Claude Code. Or download
maddi-mcp-<version>.zip from the GitHub release, unzip it, and
register its bin/maddi-mcp with the agent: claude mcp add maddi -- <dir>/maddi-mcp-<version>/bin/maddi-mcp. From source:
./gradlew :maddi-mcp:installDist, then maddi-mcp/build/install/maddi-mcp/bin/maddi-mcp.
| Tool | Does |
|---|---|
configure_project, list_projects |
a Gradle build by its directory alone; or an inputConfiguration.json (the Gradle plugin's task maddi-write-input-configuration, the Maven plugin's goal maddi:write-input-configuration, the CLI's --write-input-configuration); or directories and a class path |
analyze, run_status, stop_run |
a run in the background: a large project takes minutes. sourceSets and packages analyse part of the project |
overview |
a project, package or type in one answer: types by immutability level, modifying methods, modified fields and parameters, findings by category, per package |
verdicts, explain, findings |
the annotations of types and members, the properties behind them, why they hold, the findings with their why-chains |
element_at |
the element at a file and line, explained |
diff_runs |
what changed between two runs: analyse, edit, analyse again, compare |
library_annotations |
what the last run knows of JDK and library types |
A Gradle build needs no maddi plugin: given its directory, the server compiles every Java source set with javac's
arguments logged (an init script, run with the build's own wrapper) and has the daemon turn that log into the
configuration, as the CLI's --compile-log does, with the build files hashed at that moment. The first analyze
does this (or analyze with configureOnly); it takes a full compile.
A configured project is stored (in ~/.maddi/mcp, or --store), with the hashes of the build files that shaped
it, and survives a restart. Every run first hashes those files again. When one changed, the configuration is
computed again before the run: with the plugin's task or goal, or with the command given at configuration (a
CLI-written file needs one). Computing it for a large build takes minutes; this way it happens only when the build
changed. A project configured from directories has no such command: a change to its Gradle or Maven files fails
the run, unless analyze is told allowStale.
A project can name the JDK its daemon runs on (configure_project with project and daemonJdk, a JDK 25 or later),
when the server's does not suit it: an annotation processor such as Lombok runs inside the daemon's javac, and an
older one fails on a newer JDK, leaving its source sets parsed without the members it generates. run_status says so
when it happened.
A run that finished, or was stopped with values, is stored too, the last 8 per project: after a restart its
verdicts can still be asked and compared with diff_runs. A stored run says when it was analysed and how many source
files changed since.
Every answer carries the run's certainty. Only FINAL values are certified; PROVISIONAL (a run going on or
stopped) and BEST_AVAILABLE values hold, but may be weaker than the code allows.
explain says why a verdict holds, as a chain of statements (whys), the element's own first, then into what it
calls: why a method, field or parameter is @Modified, why something is @Dependent or @Nullable, why a method has
@StaticSideEffects. For a type, why it stops where it does on the way to @Immutable (@Mutable, @FinalFields,
@Immutable(hc=true)): the first rule it fails, at the member that fails it. And why a type is not @Container
(marked "holds": false): one step per parameter that is modified, at the statement that modifies it. The IDEs show
that last one only for a type at most 2 parameters away from @Container; the server gives it for every type.
The server runs until the agent closes its standard input, and its daemon exits with it. After an installDist,
reconnect the agent to the server (/mcp in Claude Code) to run the new build. Options, each also an environment
variable:
| Option | Environment | Default |
|---|---|---|
--jdk |
MADDI_JDK_HOME |
the JDK the server runs on; the daemon needs 25 or later |
--daemon-install |
MADDI_DAEMON_INSTALL |
the bundled daemon/ directory |
--xmx-mb |
MADDI_DAEMON_XMX_MB |
the daemon's own default heap |
--log |
MADDI_DAEMON_LOG |
maddi-mcp-daemon-<pid>.log in the temporary directory |
--store |
MADDI_MCP_STORE |
~/.maddi/mcp |
How the server is built on the daemon, and what comes next (asking what an edit would do to the verdicts, and
whether it would break a contract, before making it): docs/roadmap/mcp-server.md.
Check out the three repositories side by side, as described in maddi's README:
./gradlew build
./gradlew :maddi-cli:installDist # the `maddi` CLI, in maddi-cli/build/install/maddi
./gradlew :maddi-ide-daemon:installDist # the daemon the IDE plugins bundle
./gradlew :maddi-mcp:installDist # the MCP server, with that daemon in daemon/release-cli.sh <tag> builds both zips and publishes them, with their checksums, on the GitHub release for that tag.
dogfood/ is a standalone build that applies the Gradle plugin to maddi's own CST. It is the proving ground for
eventual immutability; see dogfood/README.md.
| Repository | Holds | Depends on |
|---|---|---|
| maddi | the base: CST, front ends, printers, the AnalysisEngine interface |
nothing above it |
| maddi-mod | the analyses | the base only |
| maddi-dist | what users install | compiles against the base only; carries maddi-mod at run time |
Every module here reaches the analyses through io.codelaser.maddi.analysis.api.AnalysisEngine. maddi-mod modules
may appear only in runtimeOnly configurations, or shadeRuntime in the plugins. The maddi-tier-guard plugin, in
maddi's build-logic, fails the build when a compile class path resolves one.
Licence: LGPL-3.0-or-later (COPYING, COPYING.LESSER), as maddi.