Skip to content

feat: flag hallucinated Maven dependencies in pom.xml - #30

Open
frankreyesgarcia wants to merge 1 commit into
chains-project:mainfrom
frankreyesgarcia:hall
Open

feat: flag hallucinated Maven dependencies in pom.xml#30
frankreyesgarcia wants to merge 1 commit into
chains-project:mainfrom
frankreyesgarcia:hall

Conversation

@frankreyesgarcia

Copy link
Copy Markdown
Member

When Claude adds a <dependency> to pom.xml, the hook already blocks it if the pinned version is outdated. This adds a second check: block it if the groupId: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:

  • 404 → the coordinate doesn't exist → hallucinated package
  • 200, pinned version absent from the <versioning><versions> list → hallucinated version
  • 200, version present → fine (an old-but-real version is still handled by the existing outdated check)
  • any other status, transport error, unparseable XML, or an empty version list → inconclusive, 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 hook that blocks writes. maven-metadata.xml is 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/maven behind an injected ExistenceChecker (MavenCentralExistenceChecker), mirroring how githubactions.Checker takes a separate Sha resolver; a nil ExistenceChecker leaves only the outdated-version comparison. mismatch.Kind (KindOutdated / KindMissingPackage / KindMissingVersion) tags each report. yul scan picks this up for free (it runs each checker with before="").

Before / after

Before — only outdated pins were caught:

outdated dependencies, use these versions instead:
  org.json:json  20240303 -> 20260522

After — a made-up coordinate or version is caught too:

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

Benchmark

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 (9 real, 8 fake packages, 4 fake versions); precision 1.0 / recall 1.0, stable across runs.

🤖 Generated with Claude Code

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 algomaster99 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can reuse git-pkgs API NewClient for example to know if the package is hallucinated or not.

Comment thread pkg/maven/existence.go
// under Claude Code's PreToolUse timeout.
defaultExistenceTimeout = 10 * time.Second

mavenCentralBaseURL = "https://repo1.maven.org/maven2"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduce this. For a larger benchmark, I recommend looking for papers/dataset on arXiv or zenodo.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a related work: https://arxiv.org/abs/2605.06279.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants