Skip to content
8 min read

How do you stop package hallucinations in generated code?

Package hallucinations can turn a quick AI suggestion into an unreviewed dependency. Verify identity, maintainers, releases, scripts, and lockfile diffs.

How do you stop package hallucinations in generated code?
Table of Contents

AI coding assistants are very good at producing the two lines that make a dependency feel inevitable: an import and an install command. They are also capable of inventing the package behind those lines. If your team treats generated code as an answer rather than a proposal, a made-up name can become a real package request, then a real install, then somebody else's code running in a developer machine or a production build.

This is not a reason to ban assistants. It is a reason to separate code generation from dependency selection. I want agents to write boring glue code, tests, migrations, and first drafts. I do not want them to receive standing authority to expand the software supply chain. That distinction saves a surprising amount of cleanup.

AI can invent a dependency that the registry will happily resolve

A coding assistant predicts text. It does not consult a registry unless the workflow explicitly gives it that tool and requires it to use the result. Ask for an obscure parser, a niche cloud SDK, or a convenience wrapper around a popular library and it may return a package name that sounds exactly right. Sometimes the name does not exist. Sometimes it is a typo. Sometimes an unrelated package owner registered it. The dangerous version is the one that exists and installs cleanly.

That failure has a predictable path. An engineer asks for code that converts a file format. The assistant emits import convert from "file-convert-pro" and adds the package to package.json. The reviewer sees working-looking code, recognizes the general problem, and approves before running the application. A package with that name now has an audience: anyone who copies the answer or merges the pull request.

The attacker does not need to break your registry account or compromise an existing popular package for this to hurt you. A package that resembles the intended dependency can be enough. npm's own threat guidance calls out typosquatting and dependency confusion as attempts to make developers install a similarly named package. npm also says it detects and blocks some typosquat attempts. "Some" is the operative word. Your review process still owns the decision to add code from a new publisher.

Do not blur a nonexistent package and a malicious package. A nonexistent name is a correctness failure that stops when the install fails. A malicious or unwanted package is a supply chain failure that can pass tests, satisfy the import, and sit in your tree until it gets a chance to act. The same first control catches both: verify the dependency before installation.

A package name is an identity claim, not a search query

A package name answers "what code will we install?" It does not answer "what library did the author intend?" Those are different questions, and teams get hurt when they pretend otherwise.

The common failure is similarity bias. A reviewer sees a familiar stem such as openai, stripe, react, auth, or logger, assumes the rest is a known extension, and moves on. That is precisely why a convincing typo or invented wrapper works. npm's package naming guidance specifically tells publishers not to choose unscoped names that look similar to another package or confuse authorship. That policy recognizes the ambiguity. It does not remove your need to inspect the exact name.

Before you add a direct dependency, answer these questions in the pull request description:

  • What capability does this package provide that our existing dependencies do not?
  • What official project, repository, or vendor documentation led us to this exact package name?
  • Why do we need this package rather than calling the underlying library or service directly?
  • Who owns future upgrades and security notices for it?

The second question matters most for generated code. "The model suggested it" is not provenance. "Its README says it wraps the API" is not enough either. Find the official documentation for the API or project and see whether it names that client. If the package claims to be an official SDK but the vendor does not acknowledge it, treat it as third-party code. That may be acceptable, but the review standard should rise immediately.

Scoped names need the same discipline. A scope can make a package look official, but it only identifies a registry namespace. It does not prove that the scope belongs to the company your team has in mind. Verify the publisher through the company's own documentation or its source organization, not through the visual comfort of an @name/package string.

I also push back on a popular shortcut: "We can install it and see if it works." Installation is an execution boundary, not a neutral preview. You can inspect registry metadata and package contents before you let it participate in a normal developer workflow. That is slower than blindly pasting a command, but it is much faster than investigating why a build runner made an unexpected network call.

Publisher history tells you what you are trusting

A new package is not automatically hostile. Plenty of useful tools begin with one maintainer and one release. But a new package should not receive the same default trust as a dependency your team has used, upgraded, and inspected for years.

Start with registry metadata. npm view prints package data from the registry, and it can query individual fields rather than dumping a page of noise. npm documents fields such as maintainers, repository details, version tags, and version-specific publication time.

Run this from a clean working directory, replacing the placeholder with the exact proposed name:

pkg="example-package"
npm view "$pkg" name version "dist-tags.latest" maintainers repository time dependencies --json

Expect JSON with fields that resemble this shape:

{
  "name": "example-package",
  "version": "1.4.2",
  "dist-tags": {"latest": "1.4.2"},
  "maintainers": [{"name": "publisher-name"}],
  "repository": {"type": "git", "url": "..."},
  "time": {"created": "...", "modified": "..."},
  "dependencies": {"another-package": "^2.0.0"}
}

The values will vary, and some fields may be absent. Absence is information. A package that claims to be a serious SDK but has no visible repository, a one-day history, and no explanation of ownership deserves a hard stop until a human can account for it.

Then inspect the publisher history in context. I look for a source repository that builds and releases the package, issue activity that looks like actual maintenance, a release pattern that matches the project, and an ownership story that does not require imagination. I do not use download counts as a security score. Counts can lag, get inflated, or simply reflect that a package name is close to something popular.

Publisher continuity matters more than a single date. Did the project move to a new organization with a clear announcement? Did a long-dormant package suddenly ship a release with a large dependency change? Did maintainers change immediately before a new version? These do not prove malice. They tell you where to spend review time.

npm provenance can provide stronger evidence about where and how a published artifact was built. npm says provenance connects a package to source code and build instructions, while also warning that provenance does not prove the package contains no malicious code. That is exactly the right interpretation. Provenance helps you verify a delivery path. It does not eliminate code review.

For packages that offer it, inspect provenance in the registry and run the signature check after installation in an isolated environment:

npm ci --ignore-scripts
npm audit signatures

npm documents npm audit signatures as a check of registry signatures and provenance attestations, with errors for missing or invalid data. Use it as an integrity signal. Do not turn a successful result into an automatic approval stamp.

Release activity has to match the job the package performs

Release activity is evidence of maintenance, not a popularity contest. A tiny formatting library may be stable for years because its job is small and its API is settled. A package that implements authentication, cloud APIs, browser automation, cryptography, native bindings, or a build tool needs a different standard. Those areas change around it even if its own API looks quiet.

Review the last few releases, not just the latest version number. Look at the gap between releases, the size of changes, whether release notes exist, and whether source tags line up with what appears in the registry. A burst of versions published within minutes can be normal during a fix. It can also signal an automated release path that nobody watches. You need the surrounding evidence.

Ask a plain question that engineers often skip: if this maintainer disappears next month, what happens to us? If the answer is "we can replace 40 lines of wrapper code," the dependency has a small blast radius. If the answer is "our login, deployment, billing, or data import path stops working," you need a stronger maintenance story and a deliberate ownership decision.

Do not demand that every package have corporate backing. That habit shuts out good open-source work and pushes teams toward oversized vendors. Demand that the dependency's risk matches the evidence you have. A small library with transparent source and no install scripts can be reasonable. An obscure package that runs a postinstall hook, downloads a binary, and sits between your service and customer data needs much more scrutiny.

The same rule applies to official packages. An official publisher can ship a breaking release, expose a compromised token, or publish a dependency you would not have chosen. Official status removes one identity question. It does not remove every other review question.

The lockfile is the actual change, not generated clutter

Keep package approval human
Build an AI-augmented engineering process that keeps package approval with accountable people.

package.json states your intent. package-lock.json states what the package manager resolved. The second file deserves review because the build installs that tree, not the one-line story in the pull request.

npm describes package-lock.json as the exact dependency tree produced by installation and says committing it lets teammates, deployments, and CI install the same tree. GitHub makes the same practical point from the review side: its dependency graph parses manifest and lock files, and lockfiles give the most reliable view of direct and indirect versions.

A direct dependency can pull in dozens of indirect packages. That does not make the pull request wrong. It does mean "I only added one package" is not a meaningful review conclusion. Read the diff for four things:

  • The direct name and requested version match the approved package.
  • Resolved versions and registry locations are expected.
  • The change does not replace or upgrade unrelated packages without a reason.
  • The new tree does not add install-time behavior that the author failed to mention.

Use the narrowest diff first:

git diff -- package.json package-lock.json
npm ls --all
npm explain example-package

npm explain helps trace why a package sits in the tree. That matters when a reviewer sees an unexpected indirect dependency and needs to decide whether it came from the new library or from an unrelated resolution change.

Then reproduce the install from scratch. npm ci requires a lockfile, removes an existing node_modules directory, refuses a mismatch between the manifest and lockfile, and does not write either file. That makes it a better CI command than a casual npm install for checking whether the committed tree is coherent.

rm -rf node_modules
npm ci --ignore-scripts
npm audit signatures

I use --ignore-scripts for the first pass because npm says that setting prevents scripts declared in package manifests from running. It does not make a package safe, and it does not prevent an engineer from explicitly running npm run later. It gives you a controlled point to inspect the dependency graph before lifecycle hooks get their turn.

A lockfile does not protect you from a bad initial choice. It makes that choice reproducible. That is useful for debugging and terrible if nobody noticed the choice in the first place.

Installation scripts deserve their own approval decision

Dependencies can execute code before your application starts. preinstall, install, postinstall, and related lifecycle scripts run in the package-manager path, often on developer laptops and CI runners that have credentials, source code, deployment tokens, or access to internal services.

Some legitimate packages need this. Native modules may compile code. Browser tooling may fetch a platform binary. The right response is not a useless blanket ban that engineers bypass. The right response is to make script execution visible, rare, and attributable.

For a proposed dependency, inspect its package archive before a normal install when practical:

npm pack example-package --dry-run
npm view example-package scripts --json

The first command shows what npm would put in the tarball. The second asks the registry for the package's declared scripts. If a package needs a lifecycle script, require the pull request to say what it runs, why it needs to run, and which environments will execute it.

Recent npm documentation describes an allowScripts policy for project installs and a strict-allow-scripts setting that can turn unreviewed install scripts into an install failure. It also documents npm install-scripts commands that record approvals in package.json. The details matter because npm versions differ, so test this in the exact CLI version your CI uses before you make it a merge gate.

A policy fragment can make the decision reviewable alongside the dependency:

{
  "allowScripts": {
    "[email protected]": true,
    "example-package": false
  }
}

Do not copy this blindly into a repository and assume it has taken effect. Confirm behavior with your package manager version and test both an approved script and an unapproved one in CI. The failure this prevents is simple: an agent adds a package with an installation hook, the lockfile looks routine, and the hook runs wherever developers or CI execute installs.

Avoid a second bad habit: approving an entire publisher or every future version because one current version needed a script. Version-pinned approval gives you a chance to reassess a package when its release behavior changes.

Automated review can reject known bad changes, but it cannot supply judgment

Constrain AI coding workflows
Set practical boundaries for Claude Code, Codex, MCP tools, and multi-agent pipelines.

Put dependency review in pull requests, then use people for the questions automation cannot answer. GitHub's dependency review uses manifest and lockfile changes to show packages added, removed, or updated, including indirect changes, release dates, known vulnerabilities, licenses, and usage information. Its action can fail a workflow at a chosen vulnerability severity and can apply license rules.

A minimal workflow looks like this:

name: Dependency review
on: [pull_request]
permissions:
  contents: read
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high
          fail-on-scopes: runtime

Make the job required only after you have tested it against ordinary dependency updates. A gate that breaks on false assumptions will be disabled by Friday. Start by blocking known high-severity runtime additions, then adjust from real pull requests rather than from a policy document nobody has exercised.

Do not mistake a clean dependency review for proof that a package is legitimate. Vulnerability databases need time to catch up. A brand-new malicious package may have no advisory, no CVE, no reputation signal, and no known vulnerable version. Automation sees published metadata. It cannot tell whether your assistant invented a package that you never needed.

The human reviewer should therefore have one explicit job: verify that the package exists for the stated reason and comes from the intended project. That sounds modest. It catches the failure that scanners are structurally bad at catching.

Give coding agents less authority than human maintainers

The safest place for an AI assistant is inside a constrained change request. Let it propose code and tests. Let it open a pull request. Do not let it add a package, run a package manager with broad credentials, and merge its own result as one unattended loop.

I use four rules for agent-enabled repositories:

  1. An agent may modify application code, tests, and documentation without special review rules.
  2. Any direct dependency addition requires a human owner in the pull request.
  3. Any manifest or lockfile change triggers dependency review and a written reason for the change.
  4. An agent branch cannot merge a dependency change automatically, even when tests pass.

This policy targets authority, not intelligence. A careful human can still make a bad dependency choice. An excellent agent can still propose a sensible one. The merge control exists because installing third-party code changes what your organization runs, supports, and has to explain later.

Keep the rule at the direct dependency boundary. If you try to require security committee approval for every transitive version movement, engineers will batch updates for months or pin insecure packages to avoid paperwork. Review the library someone chose. Use automated policy and ordinary code review to manage the tree that follows.

This is also where startups make an expensive mistake. They grant broad agent permissions because the team is small and speed feels existential. Then they discover that one misconfigured coding environment can change source, secrets, dependencies, and deployment files in the same session. Small teams need tighter boundaries, not more informal ones.

A ten-minute dependency review prevents a long incident response

Audit agent dependency authority
In five business days, find where AI-assisted dependency changes create avoidable engineering cost.

Use a fixed review sequence whenever generated code introduces a direct package. It keeps the discussion short and prevents the team from arguing over vague feelings about trust.

  1. Confirm the exact package name in official project or vendor documentation.
  2. Run npm view and inspect repository, maintainers, publication history, and dependencies.
  3. Check whether the package declares installation scripts and inspect why.
  4. Review package.json and package-lock.json together, including indirect additions.
  5. Install with npm ci --ignore-scripts, run the test suite, and verify signatures where the registry supports them.

If any step produces a question nobody can answer, remove the dependency from the pull request until someone can. You do not need to prove that a package is malicious to reject it. You need enough evidence to justify making it part of your production software.

The fastest fix for a hallucinated package is deleting it before the first merge. The second-fastest fix is removing it before CI or a developer laptop executes its scripts. After that, you are doing incident work: reviewing token exposure, build logs, outbound traffic, deployment credentials, and every environment where the tree ran.

At AppMaster.io, reducing an engineering team does not mean handing unbounded authority to an agent. It means designing the review points so two strong engineers can move quickly without trusting every plausible line of generated code. If you are already letting agents change dependencies, Book a Team & AI Audit before that permission turns into an unowned supply chain decision.

The policy must fit the team that will enforce it

A dependency policy fails when it asks a five-person company to behave like a regulated enterprise with a dedicated security team. It also fails when it says "use judgment" and gives no one a repeatable way to exercise it.

For an early-stage company, I would keep the written policy to one page. State that agents cannot merge dependency additions. Require an owner and the five review steps above. Make the dependency review job required. Keep installs in CI reproducible with a committed lockfile. Record exceptions in the pull request rather than in a separate spreadsheet that dies after two weeks.

For a larger engineering group, assign code owners for manifests, package manager configuration, and build workflows. Track direct dependencies centrally and set an expiry date for temporary exceptions. The goal is not a perfect inventory on paper. The goal is knowing who accepted each direct dependency and why it still belongs.

Do not set a policy that says every package needs provenance, because you will reject useful packages that cannot provide it and teach engineers to hide changes. Require provenance when it exists, especially for packages that build artifacts or touch sensitive paths. Treat its absence as a review input, not a magic risk score.

The same restraint applies to release recency. A package should not be rejected just because it has not published recently. Reject it when your team cannot explain whether it is stable, abandoned, or simply unnecessary. Most invented dependencies fail that test immediately, which is why the process works.

Every dependency is a maintenance commitment. An AI assistant can draft the import in seconds. Your team still owns the code that import pulls into every laptop, build runner, and deployed service.

Frequently Asked Questions

What is a package hallucination in AI-generated code?

Treat the package name as untrusted input until a person verifies it. Check that it exists, matches the intended library, has a credible source repository, and appears in the lockfile exactly as expected. A plausible name is not evidence that the package is legitimate.

Can AI invent a package that looks legitimate?

Yes. The risk is not limited to obscure packages because an assistant can misspell a well-known name, confuse similarly named libraries, or invent a wrapper that sounds useful. The more quickly a team accepts generated code, the easier it is for that error to reach an install command.

How do I verify an npm package before installing it?

Run npm view against the exact name and inspect the repository, maintainers, release history, dependency list, and dist information. Then confirm that the project documentation or official source actually recommends that package. Do not accept a package just because an AI response included an import statement.

Does package-lock.json make an invented dependency safe?

No. A lockfile pins what your build installs after someone has accepted a dependency, but it does not prove that the original choice was correct or safe. Review the manifest and lockfile together, because the manifest explains intent and the lockfile shows the resolved tree.

What should I review in a lockfile diff?

Inspect every added or changed resolved package, version, registry URL, integrity value, and lifecycle script implication. Also look for unrelated transitive churn, because a one-line manifest change can alter far more of the installed tree than the author intended.

Is an old npm publisher account automatically trustworthy?

Publisher history is useful evidence, not a verdict. A long-lived maintainer account can still be compromised, and a new package can still be honest. Use history alongside source visibility, release discipline, provenance, script behavior, and whether the package solves a real problem.

Should I allow postinstall scripts from new dependencies?

Do not let an unreviewed dependency run installation scripts on a developer laptop or CI runner. Start with npm ci --ignore-scripts when you need to inspect a suspicious change, then approve only scripts that the team understands and needs.

Can GitHub dependency review catch package hallucinations?

Dependency review can surface added packages, release dates, known vulnerabilities, licenses, and indirect changes in pull requests. It cannot tell you whether the direct dependency was invented, unnecessary, or a near miss of the library you actually meant to use. A reviewer must still check identity and purpose.

What policy should a startup use for AI-added dependencies?

Ban direct dependency additions from unattended agent branches, require a human owner for each addition, and make dependency diffs a required review category. Keep the policy narrow enough that engineers follow it instead of routing around it with copied tarballs or untracked scripts.

When should I get outside help with AI supply chain controls?

Start with the repositories that let coding agents open pull requests or run package manager commands. A Team & AI Audit can map those paths, find where dependency review is absent, and identify which controls will reduce risk without turning every package update into a security committee meeting.

Related Posts