Skip to content

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

Open
frankreyesgarcia wants to merge 4 commits into
chains-project:mainfrom
frankreyesgarcia:hall
Open

frankreyesgarcia wants to merge 4 commits 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 Outdated
// 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.

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.

Good catch — a real coordinate could be published to a repo Central has never heard of (Jenkins' update center, Atlassian's public repo, a corporate Nexus/Artifactory mirror), which this check would misread as hallucinated.

Rather than reaching for a wider (and much less deterministic) multi-registry lookup, I've pushed a change on claude/pr-review-comments-dma255 that has the existence check back off entirely whenever the pom.xml declares its own <repositories> — in that case, "not on Central" is no longer evidence of hallucination, so the check fails open for the whole file rather than risk a false positive. See declaresCustomRepositories in pkg/maven/pom.go (commit 8d8a936).


Generated by Claude Code

Comment thread benchmark/hallucination/fixtures.jsonl Outdated
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.

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.

Trimmed fixtures.jsonl from 21 hand-built cases down to 9 (4 real, 3 fake-package, 2 fake-version) — just enough to cover each failure mode the check makes, on claude/pr-review-comments-dma255 (commit 8d8a936).

On the arXiv dataset: this environment's network egress blocks arxiv.org, so I couldn't fetch the paper to pull real data from it. I've added a README pointer to it as the recommended source for a larger-scale precision/recall run instead of growing this file by hand further — someone with access to the dataset would be better placed to wire it in properly.


Generated by Claude Code

frankreyesgarcia and others added 2 commits September 9, 2026 10:48
RegistryExistenceChecker (git-pkgs/enrichment's registries client, which
queries Maven Central's Solr search index) can return found/not-found
inconsistently for the same coordinate shortly after a version is
published - observed as a real false positive ("hallucinated version")
against a genuinely published release while benchmarking yul against real
projects. Existence now retries a registries.ErrNotFound, and separately a
queried version missing from an otherwise-real version list, once (300ms
backoff) before trusting either as a real miss.

Bumped github.com/git-pkgs/registries to v0.9.1, which includes upstream
PR git-pkgs/registries#82 fixing a related (but distinct) bug: versions
v0.8.1 and earlier decoded the wrong JSON field for Maven's core=gav Solr
response shape, so every returned version came back as an empty string.

Removed the old fixture-based hallucination benchmark (README/fixtures/
run.sh) - it fed crafted payloads straight to the yul binary and never
touched a real registry, so it couldn't have caught either bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…BUMP

Adds benchmark/bump/, a harness that extracts a project's pre-breaking
state from chains-project/bump's Docker images and asks Claude to
"Upgrade the <group>:<artifact> dependency" (no "latest"/"outdated"
wording, so the prompt itself doesn't do the registry lookup for Claude),
once with yul wired up as a hook and once without, from the same
extracted source.

Ran 50 cases (100 sessions): benchmark/bump/results/50-cases-summary.md.
13/50 saw yul flag something during the hook run; a follow-up check
against Maven Central's actual current latest found the outdated-version
resolver missed real staleness in ~51% of the cases where Claude changed
the version at all - filed as chains-project#39 (root cause: the
resolver routes through ecosyste.ms for a plain PURL, and that mirror
lags behind real registries for many packages).

Also includes the single-case pilot (00a7cc31-mina-core) that surfaced
the existence-check false positive fixed in the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented Sep 9, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic Password 03a4b30 benchmark/bump/runs/04c07b06-slf4j-api/hook/transcript.jsonl View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

benchmark/bump/prompts50.md - one row per case, expanding the
"Upgrade the <group>:<artifact> dependency in this project." template
used in benchmark/bump/run_bump_case.sh with each case's actual
dependency, for reference alongside results/50-cases-summary.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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