Safeguard
Supply Chain Security•

Install Script Policy After tensorlake: npm, pnpm, Yarn and Bun

After tensorlake, set an install script policy: allowlists, release cooldowns and frozen CI installs for npm, pnpm, Yarn and Bun, plus native modules.

Safeguard Security Team
24 min read

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

FactWhy it matters for policySource
The malicious code ran "during package installation via a preinstall hook"Blocking dependency install scripts stops this class of payloadTensorlake advisory GHSA-8g63-53c9-65j2
The hook ran node lib/setup.mjs, a loader that launches the payload using BunA hook can start any runtime; the policy has to stop the hookSocket
The bad version was available for about 1 hour 40 minutes, 01:12 to 02:54 UTC on 8 October 2026A cooldown measured in days would never have resolved itTensorlake advisory
The fix PR reverted to the last clean commit, removing the preinstall hook and the added lib/ filesThe clean release did not declare that hook, so a new install script was itself the signalTensorlake 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 hookPublishers can apply the same controls to their own pipelinesTensorlake 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.

npmpnpmYarn (Berry)Bun
Dependency install scripts by defaultBlocked from npm 12; 11.16.0 and later warn onlyBlocked since pnpm 10Blocked from Yarn 4.14Blocked except Bun's default trusted list
AllowlistallowScripts in package.json, via npm approve-scriptsallowBuilds in pnpm-workspace.yaml (v11+), via pnpm approve-buildsdependenciesMeta with built: truetrustedDependencies in package.json, via bun pm trust
Unreviewed script fails the installstrict-allow-scripts=truestrictDepBuilds (default true in current docs)No separate switch documentedNo separate switch documented
Cooldown settingmin-release-age (npm 11.10.0+)minimumReleaseAge (v10.16.0+)npmMinimalAgeGate (Yarn 4.10+)minimumReleaseAge in bunfig.toml (Bun 1.3+)
Cooldown unitDaysMinutesDuration string, such as 1dSeconds
Cooldown defaultOff1440 minutes (one day) since v111d from Yarn 4.15, per release notesOff
Exemptionsmin-release-age-exclude (names or globs)minimumReleaseAgeExclude (names, patterns, exact versions)npmPreapprovedPackages (descriptors or globs)minimumReleaseAgeExcludes (names or name@version)
Frozen install for CInpm cipnpm 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

  1. Default deny for dependency install scripts.
  2. A short allowlist committed to the repository, changed only through code review, with pinned approvals where the tool supports them.
  3. A release cooldown with a written path for urgent fixes.
  4. 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.

  1. Check your version. Node.js bundles its own npm, so do not assume.

    npm -v
    npm install -g npm@12
    
  2. See what is pending, without changing anything. Your lockfile also records hasInstallScript for any package with a preinstall, install or postinstall script.

    npm approve-scripts --allow-scripts-pending
    jq -r '.packages | to_entries[] | select(.value.hasInstallScript) | .key' package-lock.json
    
  3. 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 existing false entries always win over a later approval.

    npm approve-scripts sharp canvas
    npm deny-scripts core-js
    
  4. 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
      }
    }
    
  5. Make unreviewed scripts fail and add a cooldown in the project .npmrc. min-release-age is in days; the exclude list takes names or minimatch globs.

    strict-allow-scripts=true
    min-release-age=3
    min-release-age-exclude=@yourorg/*
    
  6. Use npm ci in CI. It requires package-lock.json, errors instead of updating it, removes existing node_modules, and never writes package.json or 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.

  1. Check your version with pnpm -v.

  2. Review pending builds. Run it interactively, or name packages; a ! prefix denies. Decisions go to allowBuilds in pnpm-workspace.yaml.

    pnpm approve-builds
    pnpm approve-builds esbuild '!core-js'
    
  3. Commit the policy. minimumReleaseAge is in minutes, so 4320 is three days.

    allowBuilds:
      esbuild: true
      core-js: false
    strictDepBuilds: true
    minimumReleaseAge: 4320
    minimumReleaseAgeExclude:
      - '@yourorg/*'
    blockExoticSubdeps: true
    

    With strictDepBuilds on, the docs say the install exits non-zero "if any dependencies have unreviewed build scripts." blockExoticSubdeps lets only direct dependencies come from git or tarball URLs. Git and tarball dependencies need a repository URL or exact commit key in allowBuilds; a bare name does not approve them.

  4. Use pnpm install --frozen-lockfile in 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.

  1. Check the effective values.

    yarn -v
    yarn config get enableScripts
    yarn config get npmMinimalAgeGate
    
  2. Set the policy in .yarnrc.yml. The docs describe npmPreapprovedPackages as descriptors or globs excluded "from all of the package gates."

    enableScripts: false
    npmMinimalAgeGate: "3d"
    npmPreapprovedPackages:
      - "@yourorg/*"
    
  3. Allow specific builds in the root package.json. With scripts off, the manifest docs say "only packages with built explicitly set to true will be built."

    {
      "dependenciesMeta": {
        "sharp": { "built": true },
        "fsevents": { "built": false }
      }
    }
    
  4. Use yarn install --immutable in 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.

  1. See what Bun trusts and what it blocked.

    bun pm default-trusted
    bun pm untrusted
    
  2. Trust only what you reviewed. bun pm trust runs the scripts and adds the package to trustedDependencies. Use --dry-run first.

    bun pm trust --dry-run sharp
    bun pm trust sharp
    
  3. Commit an explicit list so the policy shows up in code review. Packages from file:, link:, git: or github: sources need their own entry even if their name is on the default list.

    { "trustedDependencies": ["sharp"] }
    
  4. Add a cooldown in bunfig.toml. Bun's unit is seconds.

    [install]
    minimumReleaseAge = 259200
    minimumReleaseAgeExcludes = ["@types/bun"]
    
  5. Use bun ci in CI. It equals bun 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 3 in npm, 4320 in pnpm, "3d" in Yarn and 259200 in 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.gyp rely on npm's implicit node-gyp rebuild, which npm 12 blocks.
  • Packages that set up a platform binary in a postinstall step. npm's docs use sharp and canvas as approval examples; pnpm's use esbuild.
  • Git dependencies with a prepare script, 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-scripts turns off everything.

How to handle them:

  1. 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.
  2. Read what each script does. A script that runs node-gyp rebuild is a different risk from an obfuscated file.
  3. 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.
  4. Approve with a pin, so a new version that changes its script needs a fresh review.
  5. Rebuild after approving with npm rebuild sharp or pnpm rebuild sharp, or reinstall. bun pm trust runs the script as it trusts.

Troubleshooting

SymptomLikely causeFix
Missing .node file at runtimeNative build was blockedReview, approve pinned, rebuild
npm allowlist seems to do nothingignore-scripts=true in an .npmrc takes precedenceRemove it, or accept that no scripts run
npm approve-scripts fails with ENOMATCHInfoQ commenters report this for packages not yet installedInstall first, then approve
pnpm ignores onlyBuiltDependenciesRemoved in v11Migrate to allowBuilds
Bun stops running a script that used to workYour trustedDependencies replaced the default listList the package explicitly
No matching version right after a releaseThe cooldown filtered itWait, or add a temporary exemption

Rolling it out across a team

  1. Inventory tools and versions per repository, and pin the tool version in the repository (pnpm's docs reference packageManager and devEngines.packageManager) so laptops and CI agree.
  2. Agree on one cooldown in days and translate it per tool.
  3. 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.
  4. Put the policy files under CODEOWNERS: package.json, .npmrc, pnpm-workspace.yaml, .yarnrc.yml and bunfig.toml.
  5. Switch CI to frozen installs and strict mode, so an unreviewed install script fails the build.
  6. Write the exception process for urgent fixes and new native dependencies, naming who approves.
  7. Audit quarterly for dangerouslyAllowAllBuilds, dangerously-allow-all-scripts, enableScripts: true and 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 ci kept 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 ., --image and --sbom write 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 allowScripts block or a pnpm-workspace.yaml policy, run npm approve-scripts --allow-scripts-pending, run tests. The /permissions modes (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: 3 in pnpm is three minutes.
  • Approving everything to turn the build green and never reviewing. --all is for after a review.
  • Name-only approvals for packages that change often.
  • A forgotten ignore-scripts=true making 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

Last reviewed: 21 October 2026. This is a developing story; check the linked advisories for the latest status.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.