Security Β· AI Agents
πŸ“¦
npm 12 Β· pnpm 11 Β· pip Β· uv
Dependency Gates for Coding Agents
Dependency gates emblem: a cyan package box behind a pink gate bar

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

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.

ansi Β· wordmark Β· slopsquat
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—      β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
β•šβ•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘
β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β•

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.

In this catalog