The tensorlake compromise ran its payload from a preinstall hook, so the install itself was the attack. Any team on npm, pnpm, Yarn or Bun can close most of that path with three settings: an allowlist for dependency install scripts, a minimum release age, and frozen lockfile installs in CI. Here is how to set each one.
This is a follow-up to our write-up of the tensorlake npm compromise, which covers the timeline, what to rotate and why valid provenance did not help. It is one of several incidents in our October 2026 roundup. Here we go deeper on one control: who may run code on your machines during install, and how to roll that out across a team without breaking every native module.
Configuration facts come from each tool's own documentation and release notes, fetched on 21 October 2026. Package managers changed fast this year, so check the version you actually run.
The tensorlake facts that matter for policy
| Fact | Why it matters for policy | Source |
|---|---|---|
The malicious code ran "during package installation via a preinstall hook" | Blocking dependency install scripts stops this class of payload | Tensorlake advisory GHSA-8g63-53c9-65j2 |
The hook ran node lib/setup.mjs, a loader that launches the payload using Bun | A hook can start any runtime; the policy has to stop the hook | Socket |
| The bad version was available for about 1 hour 40 minutes, 01:12 to 02:54 UTC on 8 October 2026 | A cooldown measured in days would never have resolved it | Tensorlake advisory |
The fix PR reverted to the last clean commit, removing the preinstall hook and the added lib/ files | The clean release did not declare that hook, so a new install script was itself the signal | Tensorlake PR #1016 |
The same PR moved the project's CI to npm ci --ignore-scripts and added a check that fails if a published manifest declares a lifecycle hook | Publishers can apply the same controls to their own pipelines | Tensorlake PR #1016 |
Socket notes the hook ran "Where dependency lifecycle scripts are permitted." That is the whole argument for this post.
Where each package manager stands in October 2026
All four tools now have the same building blocks, but defaults, file locations and even units differ. Read this before copying a setting from one tool's docs into another's config.
| npm | pnpm | Yarn (Berry) | Bun | |
|---|---|---|---|---|
| Dependency install scripts by default | Blocked from npm 12; 11.16.0 and later warn only | Blocked since pnpm 10 | Blocked from Yarn 4.14 | Blocked except Bun's default trusted list |
| Allowlist | allowScripts in package.json, via npm approve-scripts | allowBuilds in pnpm-workspace.yaml (v11+), via pnpm approve-builds | dependenciesMeta with built: true | trustedDependencies in package.json, via bun pm trust |
| Unreviewed script fails the install | strict-allow-scripts=true | strictDepBuilds (default true in current docs) | No separate switch documented | No separate switch documented |
| Cooldown setting | min-release-age (npm 11.10.0+) | minimumReleaseAge (v10.16.0+) | npmMinimalAgeGate (Yarn 4.10+) | minimumReleaseAge in bunfig.toml (Bun 1.3+) |
| Cooldown unit | Days | Minutes | Duration string, such as 1d | Seconds |
| Cooldown default | Off | 1440 minutes (one day) since v11 | 1d from Yarn 4.15, per release notes | Off |
| Exemptions | min-release-age-exclude (names or globs) | minimumReleaseAgeExclude (names, patterns, exact versions) | npmPreapprovedPackages (descriptors or globs) | minimumReleaseAgeExcludes (names or name@version) |
| Frozen install for CI | npm ci | pnpm install --frozen-lockfile (on by default in CI with a lockfile) | yarn install --immutable (default on CI per docs) | bun ci |
The "added in" versions come from each tool's docs plus InfoQ's npm 12 coverage, which lists npm 11.10.0, pnpm 10.16, Yarn 4.10.0 and Bun 1.3 as the releases that added a cooldown. One conflict: Yarn's configuration page shows "1w" as the example npmMinimalAgeGate value, while the 4.15.0 release notes make 1d the default and later releases include fixes to how the docs describe it. Run yarn config get npmMinimalAgeGate rather than trusting either page.
What the policy should contain
- Default deny for dependency install scripts.
- A short allowlist committed to the repository, changed only through code review, with pinned approvals where the tool supports them.
- A release cooldown with a written path for urgent fixes.
- Frozen installs in CI, and no secrets in the install step.
npm: allowScripts, strict mode and min-release-age
GitHub's changelog announced that in npm 12, allowScripts defaults to off: dependency preinstall, install and postinstall scripts stop running unless the project allows them. The same rule blocks implicit node-gyp rebuild builds for packages with a binding.gyp, and prepare scripts from git, file and link dependencies. npm 12 also sets allow-git and allow-remote to none.
On npm 11.21.0 the field exists, but the docs say it is "advisory: install scripts still run by default." On npm 11, the policy below is a warning system until you upgrade or turn on strict mode.
-
Check your version. Node.js bundles its own npm, so do not assume.
npm -v npm install -g npm@12 -
See what is pending, without changing anything. Your lockfile also records
hasInstallScriptfor any package with apreinstall,installorpostinstallscript.npm approve-scripts --allow-scripts-pending jq -r '.packages | to_entries[] | select(.value.hasInstallScript) | .key' package-lock.json -
Approve what you reviewed, deny what you do not need. Approvals are pinned (
pkg@1.2.3) by default; keep that. Denials are name-only, and the docs say existingfalseentries always win over a later approval.npm approve-scripts sharp canvas npm deny-scripts core-js -
Commit the result. The version numbers here are placeholders; the command writes the ones from your lockfile.
{ "allowScripts": { "sharp@0.34.2": true, "canvas@3.1.0": true, "core-js": false } } -
Make unreviewed scripts fail and add a cooldown in the project
.npmrc.min-release-ageis in days; the exclude list takes names or minimatch globs.strict-allow-scripts=true min-release-age=3 min-release-age-exclude=@yourorg/* -
Use
npm ciin CI. It requirespackage-lock.json, errors instead of updating it, removes existingnode_modules, and never writespackage.jsonor the lockfile.
The --allow-scripts flag is for npx, npm exec and global installs only; passing it to a project install is an error. For global tools, InfoQ shows the user config route: npm config set allow-scripts=canvas,sharp --location=user.
pnpm: allowBuilds, strictDepBuilds and minimumReleaseAge
pnpm 10.0.0's release notes say dependency lifecycle scripts "are not executed during installation by default." What changed in v11 is the setting name. pnpm 10 used onlyBuiltDependencies, neverBuiltDependencies and ignoredBuiltDependencies; current docs say all three were removed in v11 in favour of allowBuilds, and point to pnpx codemod run pnpm-v10-to-v11 to migrate. If someone pasted a pnpm 10 snippet into a pnpm 11 repository, check that first.
-
Check your version with
pnpm -v. -
Review pending builds. Run it interactively, or name packages; a
!prefix denies. Decisions go toallowBuildsinpnpm-workspace.yaml.pnpm approve-builds pnpm approve-builds esbuild '!core-js' -
Commit the policy.
minimumReleaseAgeis in minutes, so 4320 is three days.allowBuilds: esbuild: true core-js: false strictDepBuilds: true minimumReleaseAge: 4320 minimumReleaseAgeExclude: - '@yourorg/*' blockExoticSubdeps: trueWith
strictDepBuildson, the docs say the install exits non-zero "if any dependencies have unreviewed build scripts."blockExoticSubdepslets only direct dependencies come from git or tarball URLs. Git and tarball dependencies need a repository URL or exact commit key inallowBuilds; a bare name does not approve them. -
Use
pnpm install --frozen-lockfilein CI. pnpm turns it on by default in CI when a lockfile exists.
Avoid dangerouslyAllowAllBuilds: true except during migration; it also covers future transitive dependencies. ignoreScripts does not stop .pnpmfile.mjs from running.
pnpm's trustPolicy: no-downgrade fails an install when a package's trust evidence drops compared with earlier releases. Worth turning on, but do not count on it for tensorlake: that release carried valid provenance from the real workflow, so whether it would fire depends on what earlier releases carried.
Yarn: enableScripts, dependenciesMeta and npmMinimalAgeGate
Yarn's 4.14.0 release notes say it "Makes enableScripts: false the default," and 4.15.0 makes a one day age gate the default. Older Yarn 4 projects, and any project that set enableScripts: true, are not covered.
-
Check the effective values.
yarn -v yarn config get enableScripts yarn config get npmMinimalAgeGate -
Set the policy in
.yarnrc.yml. The docs describenpmPreapprovedPackagesas descriptors or globs excluded "from all of the package gates."enableScripts: false npmMinimalAgeGate: "3d" npmPreapprovedPackages: - "@yourorg/*" -
Allow specific builds in the root
package.json. With scripts off, the manifest docs say "only packages withbuiltexplicitly set totruewill be built."{ "dependenciesMeta": { "sharp": { "built": true }, "fsevents": { "built": false } } } -
Use
yarn install --immutablein CI. Yarn's hardened mode, which the docs say switches on automatically for pull requests from public GitHub repositories, also checks lockfile content against the registry.
Workspaces still run their own postinstall scripts; the policy covers third party packages.
Bun: trustedDependencies and minimumReleaseAge
Bun's docs say it "does not execute arbitrary lifecycle scripts by default," running them only for allowlisted packages. The catch is Bun's built-in trusted list, which applies when package.json has no trustedDependencies field. Adding the field replaces that list instead of extending it, and an empty array means nothing runs.
-
See what Bun trusts and what it blocked.
bun pm default-trusted bun pm untrusted -
Trust only what you reviewed.
bun pm trustruns the scripts and adds the package totrustedDependencies. Use--dry-runfirst.bun pm trust --dry-run sharp bun pm trust sharp -
Commit an explicit list so the policy shows up in code review. Packages from
file:,link:,git:orgithub:sources need their own entry even if their name is on the default list.{ "trustedDependencies": ["sharp"] } -
Add a cooldown in
bunfig.toml. Bun's unit is seconds.[install] minimumReleaseAge = 259200 minimumReleaseAgeExcludes = ["@types/bun"] -
Use
bun ciin CI. It equalsbun install --frozen-lockfile. Bun does not turn frozen installs on automatically in CI, so call it explicitly.
Cooldowns: choosing a number and handling urgent fixes
tensorlake 0.5.144 was live for about 100 minutes, so a one day cooldown in any of the four tools would have skipped it. The cost is that cooldowns delay good releases too, including security fixes.
- Pick a value. pnpm's supply chain guide uses one day by default and mentions a week (10080 minutes) for stricter setups.
- Watch the units. Three days is
3in npm,4320in pnpm,"3d"in Yarn and259200in Bun. - Exempt your own scope if you publish internal packages.
- Write down the urgent fix path. Add an exemption, install, and remove it once the cooldown passes. pnpm (since v10.19.0) and Bun accept exact versions and Yarn accepts descriptors. npm's list takes names or globs, so the exemption covers every version of that package while it is in place.
- Remember a cooldown is not a scan. It buys time for someone to notice. A release that survives the window is not proven clean.
CI: frozen installs, and no secrets during install
Endor Labs reports that the tensorlake loader "checks whether it is running in CI," inspecting variables such as GITHUB_ACTIONS. Runners hold valuable secrets, so give the install step none. A GitHub Actions example; replace each placeholder with a full commit SHA:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full-commit-sha>
- uses: actions/setup-node@<full-commit-sha>
with:
node-version: 22
# Install and test with no secrets in the environment
- run: npm ci
- run: npm test
# Secrets only on the step that needs them
- run: npm run deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Swap the install line for pnpm install --frozen-lockfile, yarn install --immutable or bun ci as needed. Where a build needs no install scripts at all, npm ci --ignore-scripts is the strictest option, and the one Tensorlake adopted. The npm docs say --ignore-scripts takes precedence over the allowlist.
What breaks, and how to handle native modules
Default deny will break something. Usually it is one of these:
- Native addons compiled on install. Packages with a
binding.gyprely on npm's implicitnode-gyp rebuild, which npm 12 blocks. - Packages that set up a platform binary in a postinstall step. npm's docs use
sharpandcanvasas approval examples; pnpm's useesbuild. - Git dependencies with a
preparescript, which npm 12 blocks along with git dependencies themselves. - Your own root scripts, if you use
--ignore-scripts. npm's allowlist covers dependencies only, but--ignore-scriptsturns off everything.
How to handle them:
- Find them on a branch first with
npm approve-scripts --allow-scripts-pending,pnpm approve-builds,bun pm untrusted, or the skipped build report Yarn prints. - Read what each script does. A script that runs
node-gyp rebuildis a different risk from an obfuscated file. - Prefer prebuilt binaries. Many native projects publish one package per platform and select it through
optionalDependencies, which needs no install script. If a maintained alternative works that way, you drop an allowlist entry. - Approve with a pin, so a new version that changes its script needs a fresh review.
- Rebuild after approving with
npm rebuild sharporpnpm rebuild sharp, or reinstall.bun pm trustruns the script as it trusts.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Missing .node file at runtime | Native build was blocked | Review, approve pinned, rebuild |
| npm allowlist seems to do nothing | ignore-scripts=true in an .npmrc takes precedence | Remove it, or accept that no scripts run |
npm approve-scripts fails with ENOMATCH | InfoQ commenters report this for packages not yet installed | Install first, then approve |
pnpm ignores onlyBuiltDependencies | Removed in v11 | Migrate to allowBuilds |
| Bun stops running a script that used to work | Your trustedDependencies replaced the default list | List the package explicitly |
| No matching version right after a release | The cooldown filtered it | Wait, or add a temporary exemption |
Rolling it out across a team
- Inventory tools and versions per repository, and pin the tool version in the repository (pnpm's docs reference
packageManageranddevEngines.packageManager) so laptops and CI agree. - Agree on one cooldown in days and translate it per tool.
- Generate a starting allowlist. GitHub's guidance, as InfoQ summarised it, is to allow the scripts already in the tree, then tighten. Schedule the review that tightens it.
- Put the policy files under CODEOWNERS:
package.json,.npmrc,pnpm-workspace.yaml,.yarnrc.ymlandbunfig.toml. - Switch CI to frozen installs and strict mode, so an unreviewed install script fails the build.
- Write the exception process for urgent fixes and new native dependencies, naming who approves.
- Audit quarterly for
dangerouslyAllowAllBuilds,dangerously-allow-all-scripts,enableScripts: trueand allowlists that only grow.
What would have helped
- Default deny with pinned approvals. tensorlake 0.5.144 added a hook its clean release did not declare. On npm 12 defaults, pnpm 10 or later, Yarn 4.14 or later, or Bun with an explicit list, that hook would not have run unless someone approved it.
- A cooldown of a day or more. The release was gone before it would have qualified.
- Frozen installs. A lockfile pinned to 0.5.143 and installed with
npm cikept 0.5.143. - No secrets during install, and a fast answer to "who has this version?" as in SBOM-driven incident response.
The limits matter. Script policy does not stop code that runs when you import a package, run its CLI or run tests. A cooldown does nothing once a bad release survives the window. And this worm family also plants files such as .vscode/tasks.json in repositories it reaches, as our Shai-Hulud explainer describes. Script policy is one layer.
Where Safeguard fits
Safeguard does not detect malicious packages, and it would not have flagged tensorlake 0.5.144. It does not decide which install scripts are safe either; that is your reviewers' call.
- Inventory. Safeguard builds SBOMs and dependency inventories from repositories, container images and SBOM files, including transitive dependencies, and the platform shows which projects contain a given component and version.
- Scanning.
safeguard scan --dir .,--imageand--sbomwrite JSON results under./output. Vulnerability matching and reachability analysis help you decide which new releases are urgent enough to exempt from a cooldown. - Gates. Policies and gates evaluated in the platform can stop a build that still carries a flagged dependency.
- Rollout with approval. The Safeguard CLI's assistant can edit code and run commands with your approval: add an
allowScriptsblock or apnpm-workspace.yamlpolicy, runnpm approve-scripts --allow-scripts-pending, run tests. The/permissionsmodes (Ask, Accept edits, Plan, Auto, the default, and Strict) set how much it does without asking. Desktop and phone apps can send work to a computer running the CLI and approve each command. Autonomous remediation can open fix PRs, but it is off by default.
Zero-Day Discovery and Gold
tensorlake 0.5.144 was a malicious release, not a vulnerability, and we are not claiming either capability below flagged it.
- Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories before they reach public CVE databases. It covers seven package ecosystems. It finds flaws in code, not compromised maintainer accounts.
- Gold Open Source at gold.safeguard.sh is free with no login: searchable CVE intelligence with CVSS, EPSS, KEV status, affected and fixed versions and a recommended safe version, plus a free read API and a GitHub Action for CI gates. It fits the cooldown exception process: the affected and fixed versions tell you whether a brand new release fixes something that deserves an exemption.
- Gold Registry at registry.safeguard.sh is the paid tier: hardened drop-in components and container images held to zero exploitable critical or high CVEs, SLSA L3 or higher, Sigstore-signed, with evidence pages; an artifact loses its badge on regression. The habit it supports fits this post: pull patched, signed components from a curated source and rebuild fast, rather than taking whatever was published an hour ago.
To try Safeguard, download it and read the CLI documentation.
Common mistakes
- Mixing units.
minimumReleaseAge: 3in pnpm is three minutes. - Approving everything to turn the build green and never reviewing.
--allis for after a review. - Name-only approvals for packages that change often.
- A forgotten
ignore-scripts=truemaking you think the allowlist is active. - pnpm 10 setting names in a pnpm 11 repository.
- Deploy secrets in the install job's environment.
- Treating the policy as malware protection. Code that runs at import or test time is outside it.
FAQ
Would an install script policy have stopped tensorlake 0.5.144?
For the install-time payload, very likely, if the project denied scripts by default and nobody approved the new hook. It would not help a project that allowed all scripts or approved everything unreviewed.
Is --ignore-scripts enough on its own?
It is the strictest option and suits CI for projects without native builds. It also disables your own scripts and every dependency build step, so an allowlist is easier to keep on across a team.
Does npm block install scripts by default now?
From npm 12, yes, including implicit node-gyp builds. On npm 11.16.0 and later the same policy only warns.
What cooldown should we use?
One to three days covers short-lived malicious releases like this one. pnpm defaults to one day since v11, and Yarn to one day from 4.15. Pair any value with an exception path for urgent fixes.
Will a cooldown delay security patches?
Yes. Use your tool's exclude setting for the fixed release, then remove the exemption once the cooldown passes.
How do I handle sharp, canvas or other native modules?
Review the script, approve it pinned to your lockfile version, and rebuild. A maintained package that ships prebuilt per-platform binaries may not need an install script at all.
Should publishers do anything differently?
Yes. Tensorlake's fix added a CI check that fails if a published manifest declares an install lifecycle hook. If your package needs no install scripts, a similar check is cheap. More publisher steps are in our tensorlake write-up.
Fix it without waiting for upstream
In incidents like this one, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would have helped
These are recorded walkthroughs of real sessions. None shows the tensorlake incident; each shows a flow you would reuse here.
The first demo shows the Safeguard CLI fixing a lodash CVE: it reads the finding, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and pushes nothing. For this post, you would use the same flow to add an allowScripts block, run npm approve-scripts --allow-scripts-pending and your tests, approving each command as it comes up.
Click through it at your own pace, or open it full screen.
The second shows the desktop app: the CLI session appears in the conversation list, you ask for work on your computer, pick the folder, and Approve or Deny each command and file edit until the upgrade is done and tests pass. Rolling a script policy across repositories works the same way, one folder at a time, with each edit to .npmrc, pnpm-workspace.yaml or bunfig.toml shown before it lands.
Click through it at your own pace, or open it full screen.
The third is a real Android session that sends work to a computer running the CLI and approves a command on the phone with Allow once, Allow for turn or Deny. Approving a native module's build script is the kind of decision that deserves an explicit yes, and a reviewer can give or refuse it from the phone.
Click through it at your own pace, or open it full screen.
Quick check
Your pnpm-workspace.yaml says minimumReleaseAge: 3. How long does pnpm wait before installing a new version?
Three minutes. pnpm's unit is minutes, so three days is 4320. npm uses days, Yarn a duration string such as "3d", and Bun seconds.
On npm 12, you approve sharp by name only. A later sharp release changes its install script. Does the new script run?
Yes. A name-only entry allows any future version. That is why npm approve-scripts writes pinned entries by default, so a changed script on a new version needs a fresh review.
Sources
- Tensorlake security advisory GHSA-8g63-53c9-65j2
- Tensorlake PR #1016: revert the 0.5.144 compromise and harden the npm release path
- Socket: tensorlake compromise
- Endor Labs: tensorlake npm package compromised by Shai-Hulud
- GitHub Changelog: Upcoming breaking changes for npm v12
- npm Docs v12: npm approve-scripts
- npm Docs v12: npm deny-scripts
- npm Docs v12: config
- npm Docs v12: npm ci
- npm Docs v12: scripts
- npm Docs v12: package-lock.json
- npm Docs v11: npm approve-scripts
- npm Docs v11: config
- npm Docs v11: package.json
- InfoQ: npm 12 released
- pnpm Docs: build settings
- pnpm Docs: dependency resolution settings
- pnpm Docs: supply chain security
- pnpm Docs: pnpm approve-builds
- pnpm Docs: pnpm install
- pnpm v10.0.0 release notes
- Yarn Docs: yarnrc settings
- Yarn Docs: manifest
- Yarn Berry releases on GitHub
- Bun Docs: lifecycle scripts
- Bun Docs: bunfig.toml
- Bun Docs: bun pm
- Bun Docs: bun install
Last reviewed: 21 October 2026. This is a developing story; check the linked advisories for the latest status.