# The Package That Did Not Exist: Dependency Gates for Agent Written Code

> Coding agents invent package names that repeat. How slopsquatting works, and the five gates, from agent rules to npm 12 install defaults, that stop it.

- Canonical: https://thearchitecturedeepdive.blog/articles/dependency-gates-agent-code.html
- Series: The Architecture DeepDive, No. 029
- Author: Barnabas Waweru
- Published: 2026-10-07
- Tags: Security, AI Agents
- Cite as: Barnabas Waweru, "The Package That Did Not Exist: Dependency Gates for Agent Written Code", The Architecture DeepDive, https://thearchitecturedeepdive.blog/articles/dependency-gates-agent-code.html

---

npm 12 · pnpm 11 · pip · uv

Dependency Gates for Coding Agents

Coding agents invent package names, and they invent the same ones again. An attacker who registers those names does not need your repository, only your agent's next install. This is the architecture of that attack, from repeatable hallucination to the install hook that runs the payload, and of the five gates that stop it: the agent's rules, the lockfile review, the release age gate, the package manager's install policy, which npm 12 and pnpm 11 just tightened by default, and the sandbox that decides what a bad package can reach.

## Overview

Your coding agent is cleaning up a lint failure at 02:40. It decides the fix needs a helper library, writes a plausible name into `package.json`, runs install, sees green, and opens a pull request. Nobody typed that name. Nobody looked it up. If the package exists, it was published by somebody, and whatever its install hook does already ran on the machine that holds your agent's tokens.

That is the whole attack, and it does not need access to your code. It needs your agent to say a name out loud, and it needs that name to be predictable. The research says it is. In the largest study of the problem, Spracklen and colleagues generated 576,000 Python and JavaScript code samples with 16 models and checked every package the samples referenced: 19.7% of 2.23 million package references pointed at packages that did not exist, which added up to 205,474 unique invented names. Then they asked the same prompts again. 43% of the hallucinated names came back in all ten reruns.

A one off mistake is noise. A name that recurs almost half the time is a forecast an attacker can read, register on npm or PyPI, and wait behind. The practice has a name, slopsquatting, and in 2025 a campaign called PhantomRaven showed what the payload side looks like at scale. This piece treats install as what it now is when an agent runs it: **a trust decision that no human made**. It walks the path a fake package travels, the five places you can stop it, and what changed in 2026 when npm 12 and pnpm 11 moved several of those gates into the package manager's defaults.

#### The Name

The agent emits a package name from its weights. If it is invented, the same name tends to come back, for you and for every other user of that model.

#### The Registry

npm and PyPI accept new names from anyone. A squatted name passes every "does this package exist" check, because now it does.

#### The Resolver

The package manager follows what the manifest says, including, until npm 12, dependencies declared as remote URLs that never touch the registry.

#### The Hook

A lifecycle script runs during install, before anyone reads the code. That is where the payload executes and where your secrets are in reach.

Install Is a Trust Decision Nobody Made

Earlier pieces in this catalog covered the agent loop and the CI pipeline as separate systems. Dependency installation is where they meet. When a person adds a dependency, there is at least a moment of judgment: a search, a glance at the download count, a README. When an agent adds one inside a tool call, that moment is gone unless you build it back in. **Every gate in this article is a way of putting an owner back on a decision the agent made alone.**

## A Hallucination That Repeats

Two studies anchor the numbers. The first is Spracklen et al., "We Have a Package for You!", which built the 2024 baseline across 16 models. The second is an April 2026 reevaluation by Aleksandr Churilov that ran five 2026 frontier models from Anthropic, OpenAI, Google and DeepSeek through the same question.

| Measure | 2024 cohort (Spracklen et al.) | 2026 cohort (Churilov) |
| --- | --- | --- |
| Models | 16, commercial and open source | 5 frontier models |
| Code samples | 576,000 | 199,845 |
| Hallucination rate | 5.2% commercial average, 21.7% open source average, 19.7% overall | 4.62% to 6.10% across the five models |
| Unique invented names | 205,474 | 127 names invented identically by all five models |
| Repetition | 43% repeated in all 10 reruns, 58% more than once | Cross model intersection measured directly |
| Ecosystem skew | Higher in JavaScript output | Python above JavaScript for every model, a reversal |

The headline improvement is real: the range compressed by roughly an order of magnitude at the top end. The paper's title states the catch, "The Range Shrinks, the Threat Remains." Of the 127 names all five models invented, 109 were PyPI names and 18 were npm names, and after coordinated disclosure with registry maintainers 53 were still registrable. The whole enumeration cost $860.90 in API spend.

A few details from the 2024 study shape the defense more than the rates do. Only 13.4% of hallucinations were simple character edits of a real name, so typosquatting detectors that look for one or two character distance miss most of them. 81% of unique hallucinated names came from a single model, which is why the 2026 paper argues that "single model rate measurements understate the slopsquatting attack surface." And 8.7% were cross language: Python code suggesting JavaScript packages.

Lower Rates, Same Economics

A defender reads 5% and relaxes. An attacker reads 127 names that every major model agrees on and 53 of them unclaimed. **Attack surface is the intersection across models, not the average of any one.** If your team uses several assistants, the names they share are exactly the ones worth registering, and the price of finding them is lower than one engineer day.

## The Attack Path

Slopsquatting is a supply chain attack with an unusual first stage. The attacker does not compromise a maintainer or a build server. They ask the same models you use and harvest what comes back.

```
ATTACKER PUBLIC REGISTRY YOUR AGENT SESSION │ │ │ 1 │ prompt popular models with │ │ │ ordinary coding tasks │ │ │ keep names that do not exist │ │ 2 │ keep names that recur │ │ │ across reruns and models │ │ 3 │── publish name ─────────────────▶│ plausible metadata, │ │ │ install hook, or a remote │ │ │ URL dependency │ │ │ │ 4 │ │◀── agent writes the name ─────│ │ │ into the manifest │ 5 │ │──── resolver fetches ────────▶│ │ │ │ 6 │◀──────────── lifecycle script runs, reads env, exfiltrates ─────│ │ │ Steps 1 to 3 cost the attacker almost nothing and happen once. Steps 4 to 6 repeat for every team whose agent emits the same name.
```

01

#### Harvest

Run coding prompts against the models developers use and collect every referenced package that the registry does not know.

02

#### Rank

Keep the names that recur across reruns and across models. Repetition is the signal that real developers will see the same suggestion.

03

#### Register

Publish under those names with believable descriptions, often near a real project's naming pattern so the name reads as legitimate.

04

#### Wait

An agent somewhere writes the name into a manifest and runs install. No phishing, no access to the victim's repository.

05

#### Execute

An install hook runs the payload before any code review, on a machine that usually holds registry, cloud and CI credentials.

Notice where the decision actually happens. By the time code runs in step 5, three earlier choices were already made: the agent chose a name, the manifest accepted it, and the resolver fetched it. A defense that only inspects running code is too late. A defense that only inspects the name is too early, because a registered squat is a real package.

## PhantomRaven and the Invisible Dependency

PhantomRaven is the clearest public example of the payload half of this attack. Koi Security found 126 malicious npm packages, published from August 2025 onward, with more than 86,000 installs between them. The Hacker News and BleepingComputer both reported it from Koi's research, and both connect it to slopsquatting: the packages used names that models plausibly suggest, such as `unused-imports`, `eslint-comments` and `polyfill-corejs3`, sitting next to real tooling.

### Remote Dynamic Dependencies

The technique Koi named Remote Dynamic Dependencies is the part worth designing against. Each package's manifest declared its real payload as a dependency on an attacker controlled HTTP URL, served from `packages.storeartifact[.]com`, rather than on a registry package. npm resolved and fetched that URL at install time. A `preinstall` hook then ran it.

-   **Static view:** registry metadata and most scanners showed the package with no dependencies at all. As Koi researcher Oren Yomtov put it, "Remote Dynamic Dependencies aren't visible to static analysis."
-   **Dynamic view:** the payload lived on a server the attacker controlled, so it could change after any review and could serve different code to different requesters.
-   **Execution:** no user interaction beyond the install command.

### What it took

The reports agree on the targets: developer email addresses, CI/CD environment details and a system fingerprint including public IP, plus authentication tokens for npm, GitHub Actions, GitLab, Jenkins and CircleCI. BleepingComputer quotes Koi on three exfiltration channels: HTTP GET with data encoded in the URL, HTTP POST with JSON, and a WebSocket connection. Tokens for CI systems are the prize because they turn one compromised laptop or runner into a path into every package that runner publishes.

Zero Dependencies Was the Lie

The registry page said "0 Dependencies" and that was technically what the metadata contained. The dependency graph your build actually executed had one more node, hosted somewhere you never chose. **Any gate that reads registry metadata instead of the resolved lockfile is reviewing a document the attacker wrote.** Review what the resolver fetched, and refuse to fetch from places that are not a registry.

## Five Places to Stop It

A package travels five steps from an agent's suggestion to code running with your keys. Each step can carry a gate, and each gate sees something the others cannot.

```
agent emits name ──▶ manifest + lockfile ──▶ registry fetch ──▶ install ──▶ runtime │ │ │ │ │ ┌───┴───┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ GATE 1│ │ GATE 2 │ │ GATE 3 │ │ GATE 4 │ │ GATE 5 │ │ agent │ │lockfile │ │release │ │ scripts │ │ sandbox │ │ rules │ │ review │ │age,proxy│ │ remote, │ │ secrets │ │ │ │ in PR │ │allowlist│ │ git off │ │ egress │ └───────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ reduces noise a person sees time and known no code runs nothing worth every new name malware filter at install stealing nearby Gates 1 to 3 decide whether the package arrives. Gates 4 and 5 decide what it can do once it does.
```

| Gate | Where it sits | What it catches | What it misses | Who owns it |
| --- | --- | --- | --- | --- |
| 1\. Agent rules | Agent instructions and tool permissions | Names that do not exist yet, if the agent must look them up | Any squatted name, because it now exists | Whoever configures the agent |
| 2\. Lockfile review | Pull request, before merge | Every new name entering the tree, shown to a person; denied packages; low Scorecard projects | Installs that happen outside a pull request, such as in the agent's own session | Reviewer or team lead |
| 3\. Registry gate | Between the package manager and the registry | Brand new publishes until they age; confirmed malware | Squats registered months ago and never reported | Platform team |
| 4\. Install controls | The package manager's own policy | Install hooks, remote URL and git dependencies: the whole PhantomRaven path | Malicious code that runs on import or at runtime | Whoever owns the build |
| 5\. Sandbox | The container or VM where the agent works | Nothing is prevented; theft is bounded | Code that later ships to production | Platform team |

Two cells deserve emphasis. The agent check fails exactly when the attack has succeeded, since a registered squat exists. And a release age gate only delays: a name squatted in August still installs in October unless someone reported it.

## Gate 1: Agent Rules

### Useful, and the weakest link

Tell the agent it may not add a dependency without checking the registry, and do not give it permission to run install commands unattended. This has measurable value. In the 2024 study, models asked to judge their own suggested names did well: the best commercial model reached 89% precision and recall at spotting its own hallucinations, and two others reached 82% and 81%. One open model family managed 66%.

The study also found hallucination rates rise with sampling temperature, and that fine tuning cut one open model's rate by 83%, at the cost of a 26.1% drop on a standard coding benchmark. You do not control most of those levers in a hosted assistant. You do control what the agent is allowed to execute.

The Question the Agent Can Answer Is the Wrong One

The check an agent can run is "does this package exist?" Once an attacker has registered the name, the answer is yes. **Treat agent rules as noise reduction for names nobody has claimed yet, not as protection against names someone has.** The permission that matters is whether the agent can run install, and approve scripts, without a person.

## Gate 2: Lockfile Review

Every new dependency becomes a line in a pull request diff, including transitive ones in the lockfile. GitHub's `dependency-review-action` turns that diff into a check. It is now on `v5`, which needs Actions runner v2.327.1 or later, the repository's dependency graph, and GitHub Advanced Security on private repositories.

```
name: 'Dependency Review'
on: [pull_request]
permissions:
  contents: read
jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/dependency-review-action@v5
        with:
          fail-on-severity: moderate
          deny-packages: pkg:npm/unused-imports
          show-openssf-scorecard: true
          warn-on-openssf-scorecard-level: 3
          comment-summary-in-pr: on-failure
```

### What each setting does

-   **`fail-on-severity`** fails the check on known vulnerabilities at or above `low`, `moderate`, `high` or `critical`. The default is `low`.
-   **`deny-packages`** takes package URLs. With no version it denies every version of that package; `deny-groups` does the same for a whole namespace.
-   **`show-openssf-scorecard`** (default on) and `warn-on-openssf-scorecard-level` (default 3) surface a warning when a new dependency's project scores low.
-   **`comment-summary-in-pr`** writes the result into the pull request as `always`, `on-failure` or `never`.

The action's strengths are vulnerabilities, licenses and explicit deny lists. A brand new squat has no advisory, no license problem and no history, so the action will usually pass it. What the check does give you is a moment where every new name is on screen. Pair it with a rule the action cannot enforce for you: **a new direct dependency needs a human approval, whoever or whatever wrote the code**. Branch protection with required reviewers on manifest and lockfile paths makes that rule real.

## Gate 3: Release Age and the Registry Proxy

A release age gate refuses versions younger than a set age. It buys time for the community and the registries to report and remove malware before your resolver ever sees it. Every major package manager now has one, and they all spell it differently.

| Tool | Setting | Unit | Default | Escape hatch |
| --- | --- | --- | --- | --- |
| npm (reported from v11.10.0) | `min-release-age` | days | off | `min-release-age-exclude` (added in 12.0.0 prereleases) |
| pnpm 11 | `minimumReleaseAge` | minutes | `1440` (one day) | `minimumReleaseAgeExclude`, which `pnpm audit --fix` fills with patched versions |
| uv | `exclude-newer` | duration such as `"7 days"` | off | `exclude-newer-package` |
| Dependabot | `cooldown.default-days` plus per semver level | days | off | `cooldown.exclude` (up to 150 entries) |

Two behaviors matter in practice. npm's setting does not fail the install when the newest version is too young; Classmethod's test shows it selects an older version that satisfies the range, and that it cannot be combined with `--before`, because it is converted into a `before` date internally. And Dependabot's `cooldown` applies only to version updates, not to security updates, which is the right default: a security fix should not wait a week.

```
# .npmrc
min-release-age=7

# pnpm-workspace.yaml (pnpm 11 default shown; .npmrc is now auth and registry only)
minimumReleaseAge: 1440

# pyproject.toml
[tool.uv]
exclude-newer = "7 days"

# .github/dependabot.yml (excerpt)
    cooldown:
      default-days: 7
```

### The proxy: block what is known

Socket Firewall Free sits in front of the package manager as a network proxy. You prefix the command, as in `sfw npm install`, and it filters fetches in real time across npm, yarn, pnpm, pip, uv and cargo. The free tier blocks only confirmed malware, meaning packages verified through human review, and warns without blocking on threats its AI detection flagged but nobody has confirmed yet. It needs no API key, and Socket ships a GitHub Action for CI. uv has its own version of this idea: `malware-check = true` under `[tool.uv.audit]` checks locked packages against OSV malware advisories.

The strictest form of this gate is an internal registry that only serves an allowlist. It closes the door on every unknown name and costs a standing process to approve new ones.

A Cooldown Delays, It Does Not Detect

Every tool in the table answers one question: how old is this version? A slopsquat registered after a model started inventing a name is usually old by the time your agent asks for it. **Release age stops the fast campaigns and the hijacked releases of real packages. It does nothing for a squat that has been sitting quietly for months.** That is why the next gate matters more for this specific attack.

## Gate 4: Install Controls, Now the Default

Most install time attacks need two things: a lifecycle script that runs during install, and a fetch from somewhere other than the registry. PhantomRaven needed both. In 2026 the two largest JavaScript package managers turned both off by default.

### npm 12: scripts, git and remote URLs are opt in

npm 12.0.0 shipped on 8 July 2026. GitHub's announcement lists the breaking changes plainly: `npm install` "will no longer execute `preinstall`, `install`, or `postinstall` scripts from dependencies unless they are explicitly allowed in your project," will no longer resolve git dependencies "(direct or transitive) unless explicitly allowed via `--allow-git`," and will no longer resolve dependencies "from remote URLs, such as https tarballs (direct or transitive), unless explicitly allowed." The changelog records the matching commit: "default allow-git and allow-remote to none."

-   **The policy lives in the root manifest.** Approvals are stored in an `allowScripts` field in your project's `package.json`.
-   **Approvals pin to versions by default.** `allow-scripts-pin` defaults to true, so an approval is written as `pkg@version` and a new version must be reviewed again. Denials are stored as name only `false` entries.
-   **The tooling is namespaced.** `npm install-scripts ls` lists packages with unreviewed scripts; `approve`, `deny` and `prune` manage the list.
-   **Unapproved scripts are skipped, not fatal.** The docs say install commands "silently skip lifecycle scripts for any dependency that does not have a matching entry in `allowScripts`" and print the skipped list at the end.

```
npm install-scripts ls                   # what wants to run, not yet reviewed
npm install-scripts approve sharp        # writes sharp@<version> to allowScripts
npm install-scripts deny telemetry-pkg   # writes a name only denial
npm install-scripts prune --dry-run      # find stale entries
```

### pnpm 11: supply chain protection on by default

pnpm's 11.0 release notes headline "Supply-chain protection on by default." Three defaults changed: `minimumReleaseAge` is `1440`, `blockExoticSubdeps` is `true`, which, per the pnpm 10.26 notes that introduced it, blocks "exotic protocols (like `git+ssh:` or direct `https:` tarballs) in transitive dependencies" while still allowing them for direct dependencies, and `strictDepBuilds` is `true`, so an unreviewed build script is an error rather than a silent skip. The older `onlyBuiltDependencies` family of settings was consolidated into one `allowBuilds` map of package patterns to booleans. The escape hatch, `dangerouslyAllowAllBuilds`, is named for what it does; pnpm's docs warn it lets every dependency, transitive ones included, run install scripts "both now and in the future."

### Python: hashes and wheels

pip has no lifecycle hooks in the npm sense, but building a source distribution runs code. pip's secure installs guide gives two levers. `--require-hashes` turns on hash checking mode, which requires every requirement to be pinned with `==` (or a URL or path) and hashed, including transitive dependencies you did not list; supplying any `--hash` turns the mode on globally. `--only-binary :all:` refuses source distributions entirely. With uv, install from the lockfile and refuse builds, and turn on its malware check.

| Control | npm 11 | npm 12 | pnpm 11 | pip |
| --- | --- | --- | --- | --- |
| Dependency install scripts | run by default | blocked unless in `allowScripts`, skipped with a list | unreviewed builds fail (`strictDepBuilds`) | source builds run unless `--only-binary :all:` |
| Remote URL dependencies | resolved | blocked unless `--allow-remote` | blocked for transitive deps (`blockExoticSubdeps`) | must be pinned and hashed in hash checking mode |
| Git dependencies | resolved | blocked unless `--allow-git` | blocked for transitive deps | must be pinned and hashed in hash checking mode |
| Release age | opt in, days | opt in, days, with excludes | one day by default | not in pip; uv has `exclude-newer` |

The Defaults Just Killed the PhantomRaven Path

Under npm 12 or pnpm 11 defaults, a PhantomRaven style package installs and does nothing: its remote URL dependency is not resolved and its `preinstall` hook does not run. That is a large change for one major version. **But the attack moves to the next place code executes, which is import time and runtime, and to the moment someone approves a script.** Gate 4 removes the cheap path. It does not make an unknown package safe to import.

## Gate 5: The Sandbox Decides What a Bad Package Finds

### Assume one gets through

Run agent sessions, and every install they trigger, in a container or VM that holds no production credentials, no long lived cloud keys in environment variables and no registry publish token. Limit outbound traffic to the registry or your proxy. Nothing here stops a bad package from installing. It decides whether the bad package finds anything worth stealing, and PhantomRaven's target list (npm, GitHub Actions, GitLab, Jenkins and CircleCI tokens, CI environment details) tells you exactly what to keep out of reach.

-   **Short lived identity:** give the session a token that expires with it, issued by your secrets platform rather than copied into a dotfile.
-   **Egress allowlist:** registry and proxy hostnames only during install. A payload that cannot reach its collection server exfiltrates nothing over GET, POST or WebSocket.
-   **Separate the publish path:** the machine that installs untrusted dependencies should not be the machine that holds the token to publish your own packages.

## Failure Modes and Operational Concerns

### The agent approves its own scripts

npm 12 gives you `npm install-scripts approve --all`. An agent with shell access and a goal of "make the build pass" will find it. If approval is a command the agent can run, the gate is decorative. Keep `allowScripts` changes behind the same human review rule as new dependencies, and deny the agent's tool permissions for approval commands.

### Silent skips become runtime failures

npm 12 skips unapproved scripts and prints a list. A native module whose build was skipped installs cleanly and fails later, at import, often in a test the agent then tries to "fix." Make CI fail when the skipped list is not empty, so the review happens at the gate instead of during a debugging session. pnpm 11 already fails by default through `strictDepBuilds`.

### Exclude lists drift

Every cooldown has an exclude list, and every exclude list grows during an incident and never shrinks. Review `min-release-age-exclude`, `minimumReleaseAgeExclude`, `exclude-newer-package` and Dependabot's `cooldown.exclude` on a schedule, and use `npm install-scripts prune` for stale approvals.

### Mismatched cooldowns

If Dependabot's cooldown is shorter than the package manager's release age, Dependabot opens pull requests for versions your install will refuse to pick. Set them to the same number of days.

### Installs outside the pull request

Gate 2 only sees what reaches a pull request. The agent's session already ran install on its own machine before any diff existed. That is why gates 4 and 5 must apply to the agent's environment itself, not only to CI.

01

#### Detect

A denied package, a skipped script or a malware block appears in CI or proxy logs.

02

#### Scope

Find every lockfile and every agent session that resolved the name, not only the repository that alerted.

03

#### Rotate

If any hook ran, treat every token in that environment as exposed and rotate it before cleanup.

04

#### Deny

Add the package URL to `deny-packages` and the proxy blocklist, and report it to the registry.

## The Economics on Both Sides

Cost Reality Check

**Attacker discovery:** the 2026 reevaluation spent $860.90 in API calls to generate 199,845 samples across five models and find 127 names they all invent. That is the full research budget for a target list shared by most of the market.

**Attacker publishing:** registering a name on a public registry needs an account, not a payment. PhantomRaven ran 126 packages.

**Defender, free tier:** npm 12 and pnpm 11 defaults cost an upgrade and a one time script review. Release age settings are one line each. Socket Firewall Free needs no API key.

**Defender, paid tier:** `dependency-review-action` on a private repository requires GitHub Advanced Security. An internal allowlisted registry costs the people who maintain the allowlist, every week.

Spend Where the Attacker Is Cheapest to Stop

Discovery and publishing are nearly free for the attacker, so stopping them upstream is not your job and not your budget. Execution at install is where the attacker's whole plan depends on your defaults. **The cheapest high value move in this article is upgrading the package manager and leaving its new defaults on.**

## What to Adopt, in Order

01

#### Upgrade and keep defaults

Move to npm 12 or pnpm 11. Do not pass `--allow-remote`, `--allow-git` or `dangerouslyAllowAllBuilds` to make a build pass.

02

#### Set a release age

Seven days is a common starting point; match Dependabot's cooldown to it.

03

#### Review the diff

Add `dependency-review-action` and require a human approval for new direct dependencies and `allowScripts` changes.

04

#### Constrain the agent

No install and no script approval without a person. Registry lookup before any new name.

05

#### Empty the sandbox

No production secrets, short lived tokens, egress to the registry or proxy only.

## Where the Sources Disagree

### npm's release age setting: name and unit

npm CLI pull request #8825 proposed a `minimum-release-age` setting measured in minutes, off by default. Classmethod's hands on test of npm v11.10.0 documents the shipped setting as `min-release-age`, measured in days, converted internally to a `before` date. The npm 12 changelog refers to `min-release-age` and adds `min-release-age-exclude`. This article uses the shipped name and unit, and the version from Classmethod's test, which we could not confirm against an npm release note in this run.

### npm 12's approval command

GitHub's June 2026 announcement tells users to run `npm approve-scripts` and `npm deny-scripts`. The npm 12 documentation and changelog show those commands were namespaced under `npm install-scripts` in `12.0.0-pre.2`. This article uses the documented v12 form.

### pnpm's build approval setting

Most guides written for pnpm 10 name `onlyBuiltDependencies`. pnpm 11 replaced that family with `allowBuilds`. Check which major version a guide targets before copying its config.

## Key Takeaways

### Principles

1.  **Install is a trust decision.** When an agent runs it, someone on the team still has to own it, through policy if not in person.
2.  **Repetition makes hallucination an attack surface.** 43% of invented names came back on every rerun, and 127 names are shared by five 2026 models. Measure the intersection, not the average.
3.  **"Does it exist" is the wrong check.** A squatted name exists. Agent rules reduce noise; they do not protect.
4.  **Review what the resolver fetched, not what the registry says.** PhantomRaven showed "0 Dependencies" while pulling a payload from a URL.
5.  **Release age delays, it does not detect.** It stops fast campaigns and hijacked releases, not patient squats.
6.  **Turn off code at install and fetches outside the registry.** npm 12 and pnpm 11 now do this by default. Keep it that way.
7.  **Approval is a privileged action.** If the agent can approve its own scripts, the gate does not exist.
8.  **Fail loudly on skipped scripts.** A silent skip becomes a runtime bug that an agent will try to patch around.
9.  **Empty the room before the guest arrives.** Sandboxes with short lived tokens and narrow egress decide what a bad package can take.

### Official Sources

[Spracklen et al.: We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs (arXiv 2406.10279)](https://arxiv.org/html/2406.10279v3) [Churilov: The Range Shrinks, the Threat Remains, Re-evaluating LLM Package Hallucinations on the 2026 Frontier Model Cohort (arXiv 2605.17062)](https://arxiv.org/pdf/2605.17062) [GitHub Changelog: Upcoming breaking changes for npm v12 (24 June 2026)](https://github.blog/changelog/2026-06-24-upcoming-breaking-changes-for-npm-v12/) [npm Docs: npm CLI v12 changelog](https://docs.npmjs.com/cli/v12/using-npm/changelog) [npm Docs: npm install-scripts (v12)](https://docs.npmjs.com/cli/v12/commands/npm-install-scripts) [npm CLI: pull request #8825, minimum release age proposal](https://github.com/npm/cli/pull/8825) [pnpm Blog: pnpm 11.0 release notes](https://pnpm.io/blog/releases/11.0) [pnpm Blog: pnpm 10.26 release notes (blockExoticSubdeps)](https://pnpm.io/blog/releases/10.26) [pnpm Docs: build settings (allowBuilds, strictDepBuilds, dangerouslyAllowAllBuilds)](https://pnpm.io/settings/build) [GitHub: actions/dependency-review-action](https://github.com/actions/dependency-review-action) [GitHub Docs: Dependabot options reference (cooldown)](https://docs.github.com/en/enterprise-server@3.19/code-security/reference/supply-chain-security/dependabot-options-reference) [pip Docs: Secure installs](https://pip.pypa.io/en/latest/topics/secure-installs/) [Socket Docs: Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) [Classmethod: testing npm v11.10.0 min-release-age](https://dev.classmethod.jp/articles/npm-v11-min-release-age/) [pydevtools: Protecting against Python supply chain attacks with uv (exclude-newer, malware-check)](https://pydevtools.com/handbook/how-to/how-to-protect-against-python-supply-chain-attacks-with-uv/) [The Hacker News: PhantomRaven malware found in 126 npm packages](https://thehackernews.com/2025/10/phantomraven-malware-found-in-126-npm.html) [BleepingComputer: PhantomRaven attack floods npm with credential stealing packages](https://bleepingcomputer.com/news/security/phantomraven-attack-floods-npm-with-credential-stealing-packages)
