cd /news/ai-tools/hallucinated-packages-dependency-dri… · home › topics › ai-tools › article
[ARTICLE · art-169848] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↓ negative

Hallucinated Packages & Dependency Drift in AI-Assisted Code

A principal software engineer at Loop Origin documented how an AI coding assistant invented a plausible Composer package name for parsing VAT numbers, causing a CI build failure when the package could not be found. He warns that such hallucinated dependencies enable "slopsquatting," where attackers register invented names on public registries, and recommends treating every new dependency in AI-assisted pull requests as a review item verified against the official registry, gated by CI allowlists, audited with composer audit and npm audit, and pinned to prevent dependency drift.

by read6 min views4 publishedNov 15, 2026

During a routine review on a client's Laravel and Vue project, one pull request added a Composer package with a perfectly reasonable name for parsing VAT numbers. The code looked fine and the tests passed locally for the author, because he had copied a helper from an assistant's answer and installed the package by hand. The CI build failed with "package not found". The package had never existed. The assistant had invented a plausible name. That time the failure was harmless. The worrying part is that anyone can register a missing name on a public registry, and attackers do watch for names that assistants tend to invent.

I wrote this with help from AI tools and checked every command against Composer 2.8, npm 10 and pnpm 9. I am a Principal Software Engineer in Karachi with 9+ years across PHP/Laravel, Vue, Node and .NET microservices, and through Loop Origin I maintain production SaaS codebases where a growing share of code is AI-assisted.

📚 Series Navigation — Maintaining AI-Written Code in Production

Part 1: Reviewing AI-Generated Pull Requests: A Checklist That Catches Real Bugs Part 2: Characterization Tests Before You Refactor AI-Written Code Part 3 (This Post): Hallucinated Packages & Dependency Drift in AI-Assisted Code Part 4: Static Analysis Gates: PHPStan, TypeScript Strict & Coverage in CI Part 5: Decision Records So AI Context Doesn't Rot Part 6: Measuring AI Coding Impact With DORA Metrics, Not Vibes Related earlier posts: Subresource Integrity & CDN Script Pinning · Cron Jobs That Never Ran: Heartbeats & Dead-Man Switches

Direct answer: Treat every new dependency in an AI-assisted PR as a review item. Verify the package exists on the official registry, check its age, maintainers and downloads, require lockfile changes to be reviewed, block unapproved packages with an allowlist in CI, run composer audit and npm audit, and pin versions so drift only happens through deliberate update PRs.

It helps to separate them because the fixes differ.

Hallucinated packages are names that do not exist, or did not exist when the model was trained. Researchers have measured this: a 2024 study of code-generating models ("We Have a Package for You!", Spracklen et al.) found a meaningful share of suggested packages did not exist, and many invented names repeated across prompts. Repeated names are what make "slopsquatting" possible: someone registers the invented name and waits.

Dependency drift is quieter. The assistant suggests "^2.0" where you had "2.4.1", adds a second HTTP client because it did not notice the one you already use, or pulls an old major version from its training data. Nothing breaks today. Six months later you have three date libraries and a security advisory on one of them.

Signal Hallucinated / typo package Legit but risky Healthy dependency
Exists on Packagist / npm No, or created very recently Yes Yes
Age of first release Days or weeks Years, but last release long ago Years, steady releases
Maintainers One new account One inactive maintainer Several, known org
Weekly downloads Near zero Declining Stable or growing
Install scripts ( postinstall ) Often present Sometimes Rare, documented
Source repo link Missing or mismatched Archived Active, matches package

No single row decides it. A brand-new package with one maintainer, an install script and no repository link should stop the PR until someone checks it by hand.

Most teams collapse composer.lock and package-lock.json in the diff view, which is exactly where surprises hide. I add a small CI step that prints a readable summary of what changed:

#!/usr/bin/env bash
set -euo pipefail
git fetch origin main --depth=1

echo "== Composer packages added =="
diff <(git show origin/main:composer.lock | jq -r '.packages[].name' | sort) \
     <(jq -r '.packages[].name' composer.lock | sort) | grep '^>' || echo "none"

echo "== npm packages added (top level) =="
diff <(git show origin/main:package.json | jq -r '.dependencies + .devDependencies | keys[]' | sort) \
     <(jq -r '.dependencies + .devDependencies | keys[]' package.json | sort) | grep '^>' || echo "none"

Post that output as a PR comment and reviewers see "added: acme/vat-parser" in plain text instead of a 3,000-line lockfile.

For Composer, the Packagist API gives you creation date and download counts:

pkg="acme/vat-parser"
curl -sf "https://repo.packagist.org/p2/${pkg}.json" > /dev/null \
  || { echo "BLOCK: ${pkg} not found on Packagist"; exit 1; }
curl -s "https://packagist.org/packages/${pkg}/stats.json" | jq '.downloads.total'

For npm:

npm view some-package time.created maintainers repository.url --json
npm view some-package scripts.postinstall

I fail the build when a package is missing, and flag for manual review when it is younger than 90 days or has fewer than a few thousand total downloads.

For client projects with strict needs, an explicit allowlist works well. It is just a file in the repo:

{
  "composer": ["laravel/framework", "guzzlehttp/guzzle", "spatie/laravel-permission"],
  "npm": ["vue", "axios", "pinia", "dayjs"]
}

CI compares top-level dependencies against it. Adding a package means a one-line change to the allowlist that a senior reviewer approves, which forces the conversation "do we need this, and do we already have something that does it?". That question alone has removed duplicate HTTP and date libraries from several projects we maintain.

// composer.json — prefer exact or tight ranges for production apps
"require": {
  "php": "^8.3",
  "laravel/framework": "11.31.*",
  "guzzlehttp/guzzle": "7.9.2"
}
save-exact=true
ignore-scripts=true   # enable per-package only when needed

Then let a bot (Renovate or Dependabot) open grouped update PRs on a schedule. Updates become deliberate and reviewable, not a side effect of someone pasting an assistant's snippet. ignore-scripts=true blocks install scripts by default, which removes one of the most common ways a malicious package runs code on a developer laptop or CI runner.

composer audit --locked --format=summary
npm audit --omit=dev --audit-level=high

Run both on every PR and nightly on main. Nightly matters because advisories are published after you merge.

Prevention is cheaper than detection. In the repo's assistant instructions file (whatever your tool reads, such as a rules or AGENTS.md file) list the approved libraries for common jobs: "HTTP: Guzzle. Dates: Carbon. Validation: Laravel validator. Never add a new dependency without saying so explicitly." In our experience this cuts surprise packages sharply, though it never replaces the CI checks.

save-exact, tight Composer constraints) and updated via bot PRs. composer audit and npm audit run on every PR and nightly. Common enough to plan for. Published research found that a noticeable percentage of package names suggested by code models did not exist, with open models doing worse than commercial ones. Even a small rate matters when a team merges hundreds of suggestions a month.

No. Audit tools only know about published advisories. A freshly registered malicious package has no advisory yet, which is why existence, age and allowlist checks come first.

Banning tends to fail quietly. Requiring dependency changes to be explicit, summarised and approved works better and still lets the assistant save time.

A private mirror (Private Packagist, Verdaccio, Artifactory) that only proxies approved packages is the strongest control. Make sure scoped names cannot fall through to the public registry, or you invite dependency confusion.

Start with composer show --direct and npm ls --depth=0, find duplicates doing the same job, pick one, and migrate in small PRs with characterization tests from Part 2 guarding behaviour.

Yes. We add these gates as part of our custom web application and maintenance work, usually in a day or two for a typical Laravel and Vue codebase.

Next in this series, Part 4 covers static analysis gates: PHPStan, TypeScript strict mode and coverage in CI.

── more in #ai-tools 4 stories · sorted by recency
── more on @loop origin 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/hallucinated-package…] indexed:0 read:6min 2026-11-15 · —