feat: flag hallucinated Maven dependencies in pom.xml - #30
frankreyesgarcia wants to merge 4 commits 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
left a comment
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" |
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.
There was a problem hiding this comment.
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
| {"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"} |
There was a problem hiding this comment.
Reduce this. For a larger benchmark, I recommend looking for papers/dataset on arXiv or zenodo.
There was a problem hiding this comment.
This is a related work: https://arxiv.org/abs/2605.06279.
There was a problem hiding this comment.
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
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 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
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- 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
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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>
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