feat: flag hallucinated Maven dependencies in pom.xml - #30
Open
frankreyesgarcia wants to merge 1 commit into
Open
feat: flag hallucinated Maven dependencies in pom.xml#30frankreyesgarcia wants to merge 1 commit into
frankreyesgarcia wants to merge 1 commit into
Conversation
A newly added <dependency> whose groupId:artifactId - or whose pinned <version> - was never published to Maven Central is now blocked (exit 2) as a hallucinated coordinate, not just when it's outdated. Existence is decided by a single GET of https://repo1.maven.org/maven2/<group as path>/<artifact>/maven-metadata.xml: 404 means the coordinate doesn't exist, 200 means it does and the <versioning><versions> list it carries is checked for the pinned version. Any other status, a transport error, unparseable XML, or an empty version list is inconclusive and fails open. This deliberately does not go through git-pkgs/enrichment's registry client: that queries Maven Central's Solr endpoint (search.maven.org) first, which returns found/not-found/timeout inconsistently for the same coordinate and hands back paginated, sometimes-incomplete version lists - fine for best-effort metadata, unacceptable for a blocking hook. maven-metadata.xml is served off a static CDN, carries the complete version list in one file, and is what Maven itself resolves against. The check lives in pkg/maven behind an injected ExistenceChecker (MavenCentralExistenceChecker; a nil one leaves only the outdated-version comparison), mirroring githubactions.Checker's separate Sha resolver. mismatch.Kind (KindOutdated / KindMissingPackage / KindMissingVersion) tags each report; main.go renders the missing-coordinate/version lines under "problems with new dependencies:" and still exits 2. yul scan picks this up for free via before="". benchmark/hallucination/ is a deterministic precision/recall harness for the check: crafted PreToolUse payloads fed straight to the binary, classified on the stderr reason. 21 fixtures, precision 1.0 / recall 1.0, stable across runs. Before: outdated dependencies, use these versions instead: org.json:json 20240303 -> 20260522 After: problems with new dependencies: com.example:supercache does not exist - hallucinated package, remove it or use a real coordinate com.google.guava:guava version 99.0-jre was never published - hallucinated version Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
algomaster99
requested changes
Sep 3, 2026
algomaster99
left a comment
Member
There was a problem hiding this comment.
I think you can reuse git-pkgs API NewClient for example to know if the package is hallucinated or not.
| // under Claude Code's PreToolUse timeout. | ||
| defaultExistenceTimeout = 10 * time.Second | ||
|
|
||
| mavenCentralBaseURL = "https://repo1.maven.org/maven2" |
Member
There was a problem hiding this comment.
This is not the best approach since Maven Central can have multiple repositories, for example, Jenkins, Atlassian, etc.
Comment on lines
+1
to
+21
| {"id": "real-guava", "group": "com.google.guava", "artifact": "guava", "version": "33.0.0-jre", "label": "real"} | ||
| {"id": "real-commons-lang3", "group": "org.apache.commons", "artifact": "commons-lang3", "version": "3.14.0", "label": "real"} | ||
| {"id": "real-okhttp", "group": "com.squareup.okhttp3", "artifact": "okhttp", "version": "4.12.0", "label": "real"} | ||
| {"id": "real-slf4j-api", "group": "org.slf4j", "artifact": "slf4j-api", "version": "2.0.13", "label": "real"} | ||
| {"id": "real-jackson-databind", "group": "com.fasterxml.jackson.core", "artifact": "jackson-databind", "version": "2.17.2", "label": "real"} | ||
| {"id": "real-junit-jupiter", "group": "org.junit.jupiter", "artifact": "junit-jupiter", "version": "5.10.2", "label": "real"} | ||
| {"id": "real-jsoup", "group": "org.jsoup", "artifact": "jsoup", "version": "1.17.2", "label": "real"} | ||
| {"id": "real-caffeine", "group": "com.github.ben-manes.caffeine", "artifact": "caffeine", "version": "3.1.8", "label": "real"} | ||
| {"id": "real-guava-old-version", "group": "com.google.guava", "artifact": "guava", "version": "18.0", "label": "real"} | ||
| {"id": "fake-pkg-guava-core", "group": "com.google.guava", "artifact": "guava-core", "version": "33.0.0-jre", "label": "fake-package"} | ||
| {"id": "fake-pkg-commons-http-client3", "group": "org.apache.commons", "artifact": "commons-http-client3", "version": "3.1", "label": "fake-package"} | ||
| {"id": "fake-pkg-retrofit-gson", "group": "com.squareup.retrofit", "artifact": "retrofit-gson", "version": "2.9.0", "label": "fake-package"} | ||
| {"id": "fake-pkg-spring-web-mvc", "group": "org.springframework", "artifact": "spring-web-mvc", "version": "6.1.0", "label": "fake-package"} | ||
| {"id": "fake-pkg-jackson-blackbird", "group": "com.fasterxml.jackson.core", "artifact": "jackson-databind-blackbird", "version": "2.17.2", "label": "fake-package"} | ||
| {"id": "fake-pkg-slf4j-core", "group": "org.slf4j", "artifact": "slf4j-core", "version": "2.0.13", "label": "fake-package"} | ||
| {"id": "fake-pkg-okhttp4", "group": "com.squareup.okhttp3", "artifact": "okhttp4", "version": "4.12.0", "label": "fake-package"} | ||
| {"id": "fake-pkg-gson-databind", "group": "com.google.code.gson", "artifact": "gson-databind", "version": "2.10.1", "label": "fake-package"} | ||
| {"id": "fake-ver-guava", "group": "com.google.guava", "artifact": "guava", "version": "999.0-jre", "label": "fake-version"} | ||
| {"id": "fake-ver-slf4j-api", "group": "org.slf4j", "artifact": "slf4j-api", "version": "9.9.9", "label": "fake-version"} | ||
| {"id": "fake-ver-okhttp", "group": "com.squareup.okhttp3", "artifact": "okhttp", "version": "40.0.0", "label": "fake-version"} | ||
| {"id": "fake-ver-jackson", "group": "com.fasterxml.jackson.core", "artifact": "jackson-databind", "version": "2.99.0", "label": "fake-version"} |
Member
There was a problem hiding this comment.
Reduce this. For a larger benchmark, I recommend looking for papers/dataset on arXiv or zenodo.
Member
There was a problem hiding this comment.
This is a related work: https://arxiv.org/abs/2605.06279.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When Claude adds a
<dependency>topom.xml, the hook already blocks it if the pinned version is outdated. This adds a second check: block it if thegroupId:artifactId— or the pinned<version>of a real coordinate — doesn't exist on Maven Central at all, i.e. a coordinate the model hallucinated.How existence is decided
A single GET of
https://repo1.maven.org/maven2/<group as path>/<artifact>/maven-metadata.xml:hallucinated package<versioning><versions>list →hallucinated versionThis deliberately does not go through
git-pkgs/enrichment's registry client: that queries Maven Central's Solr endpoint (search.maven.org) first, which returns found/not-found/timeout inconsistently for the same coordinate and hands back paginated, sometimes-incomplete version lists — fine for best-effort metadata, unacceptable for a hook that blocks writes.maven-metadata.xmlis served off a static CDN, carries the complete version list in one file, and is what Maven itself resolves against.Wiring
The check lives in
pkg/mavenbehind an injectedExistenceChecker(MavenCentralExistenceChecker), mirroring howgithubactions.Checkertakes a separateSharesolver; a nilExistenceCheckerleaves only the outdated-version comparison.mismatch.Kind(KindOutdated/KindMissingPackage/KindMissingVersion) tags each report.yul scanpicks this up for free (it runs each checker withbefore="").Before / after
Before — only outdated pins were caught:
After — a made-up coordinate or version is caught too:
Benchmark
benchmark/hallucination/is a deterministic precision/recall harness for the check — craftedPreToolUsepayloads fed straight to the binary, classified on the stderr reason. 21 fixtures (9 real, 8 fake packages, 4 fake versions); precision 1.0 / recall 1.0, stable across runs.🤖 Generated with Claude Code