Securing the agentic AI software supply chain
← Academy
Defense Jul 11, 2026 · 9 min

npm v12 install-time security: what changed, what holds, and what to watch

npm v12 flips three defaults that were opt-in before: dependency lifecycle scripts are blocked unless approved, git and remote URL dependencies are rejected, and 2FA-bypass automation tokens are being deprecated in favor of OIDC trusted publishing and staged releases. This guide breaks down what the changes cover, where the gaps are, and what to do now.

npm v12, now tagged latest, flips three install-time defaults that were opt-in before. Dependency lifecycle scripts no longer run unless explicitly approved. Git and remote URL dependencies are blocked. The 2FA-bypass automation tokens that most CI pipelines use to publish are being phased out in favor of short-lived OIDC credentials and a staged publishing workflow with a human approval gate.

These are the largest defensive changes to npm in years. They address real attack surfaces. They also have gaps you should understand before you rely on them.

What was wrong with the old defaults

npm install has always run lifecycle scripts automatically. When you install a package, its postinstall script fires with the same permissions as your user account. That single behavior is the attack surface behind most npm supply chain incidents: the arielsimon recon scripts, the wallet stealer that hid inside big.js, and the dependency confusion canaries all depend on the postinstall hook running without asking.

On the publishing side, most CI pipelines use long-lived automation tokens stored as secrets. If those tokens leak through a compromised CI environment, a stolen .npmrc, or a social engineering attack, anyone holding the token can publish a new version of the package. That is the scenario behind the Miasma campaign, where 23 packages were hijacked through a compromised account.

In simpler terms: the old npm trusted everything by default. It ran any code a package asked it to run at install, and it let any token holder publish without a second check.

What changed: install-time defaults

Scripts are blocked by default

Before npm v12, every dependency’s lifecycle scripts (preinstall, install, postinstall, and implicit node-gyp builds) ran automatically during installation. Now they are blocked unless explicitly approved, which is the most consequential change in the release.

When a dependency has blocked scripts, npm warns you and skips them. The package still installs and its code sits in node_modules, but the postinstall does not fire.

The approval workflow uses npm approve-scripts:

  1. Run npm approve-scripts --allow-scripts-pending to list dependencies with pending (blocked) scripts
  2. Review each script
  3. Run npm approve-scripts <package> to approve
  4. Approvals are saved in an allowScripts field in your package.json, pinned to the exact version you reviewed

When a package updates, the version pin breaks and the script is blocked again until you review the new version. This is the right default: you see what changed before it runs.

Git and remote dependencies are blocked

allow-git now defaults to "none". Dependencies that point to a git repository instead of a registry tarball are blocked unless you explicitly set the flag to "all" or "root" (root-project only). Git deps are risky because they resolve a branch or tag at install time, and the content behind that ref can change without warning.

allow-remote now defaults to "none". Dependencies that point to an HTTPS tarball URL are blocked by the same logic. Tarballs served from the same hostname as your configured registry still work normally. If your registry uses a CDN on a different hostname, you may need to adjust replace-registry-host or override the setting.

What this looks like in practice

Before (npm ≤ 11) After (npm v12) npm install Scripts run automatically Attacker payload executes npm install Scripts blocked by default npm approve-scripts Review + version pin Only approved scripts run Aephix
Before npm v12, dependency postinstall scripts ran automatically and an attacker payload fired on install. Now scripts are blocked by default. Developers review and approve each dependency through npm approve-scripts before its scripts can run.

The escape hatches

npm v12 provides several flags to override the new defaults:

  • --dangerously-allow-all-scripts bypasses the allowScripts system entirely and runs every dependency’s scripts. The name is intentional.
  • --no-allow-scripts-pin creates name-only approvals with no version pin. A package approved this way stays approved even when it publishes a new version, which means you skip the re-review.
  • --ignore-scripts (the old blunt switch from prior versions) still works and takes precedence over everything.
  • --strict-allow-scripts goes the other direction: it turns warnings into hard errors, so any unapproved script fails the install rather than being silently skipped.

These exist because the ecosystem is large and not every project can adopt the new flow immediately. They are pressure-relief valves, not recommended practice.

What changed: token security

The 2FA-bypass deprecation

The 2FA-bypass granular access token (GAT) is the token type most CI pipelines use to publish npm packages. It was designed to let automated systems skip the 2FA prompt. npm is phasing it out in two stages.

Phase 1 (early August 2026). 2FA-bypass tokens lose the ability to perform sensitive account and package operations: creating or deleting tokens, changing passwords and 2FA configuration, managing package access and maintainers, and modifying organization membership. The token can still publish directly.

Phase 2 (around January 2027). 2FA-bypass tokens lose direct publishing. They can still read private packages and stage a publish, but the staged package only goes live after a human approves it with 2FA.

What replaces them

Trusted publishing (OIDC). Instead of storing a long-lived token in your CI secrets, you register your CI provider as a trusted publisher for your package on npmjs.com. When the CI workflow runs, it generates a short-lived OIDC token, exchanges it with the npm registry for a temporary publish credential, and publishes. The credential expires after the workflow. There is nothing to steal and nothing to rotate.

Supported providers today: GitHub Actions (github.com-hosted runners only), GitLab CI (gitlab.com shared runners only), and CircleCI (cloud only). Self-hosted runners are not supported in any provider.

Setting up OIDC on GitHub Actions requires one permission in your workflow:

permissions:
  id-token: write
  contents: read

The npm CLI detects the OIDC environment automatically and handles the token exchange. You run npm publish as before.

Staged publishing. An alternative for teams that want a human gate on every release. A CI workflow runs npm stage publish (no 2FA needed) to submit the package to a staging area. A maintainer then reviews the staged package and runs npm stage approve <stage-id> (which requires 2FA) to make it public. Automation prepares the release, but a human with 2FA approves it.

Before (automation tokens) After (OIDC trusted publishing) Long-lived token in CI secrets CI runs npm publish If token leaks, attacker publishes until revoked CI generates OIDC token (ephemeral) Exchanged for short-lived credential Credential expires after workflow Cannot be extracted or reused Aephix
Long-lived automation tokens persist in CI secrets and can be stolen. OIDC trusted publishing replaces them with ephemeral credentials scoped to a single CI run.

Strengths

Scripts blocked by default closes the top attack vector. The postinstall hook is the mechanism behind most npm malware. Blocking it by default means an attacker’s payload does not fire even if the package is installed. The single highest-impact change in npm security in years.

Version-pinned approvals force re-review. When you approve package@1.2.3, the approval expires when the package updates to 1.2.4. If an attacker compromises the package and publishes a new version, the script does not run until someone reviews it. Meaningfully better than a name-only allowlist.

OIDC replaces long-lived secrets with ephemeral credentials. A stolen CI token can publish malware for months before anyone notices. An OIDC credential works for one CI run and cannot be extracted or replayed. This eliminates the most common token-theft path.

Staged publishing adds a human gate. Even if a CI pipeline is compromised, the attacker can only stage a publish. A human with 2FA must approve it. Defense in depth for the publish path.

Provenance attestations are automatic. When publishing via OIDC from GitHub Actions or GitLab CI, npm automatically generates provenance attestations linking the published package to its source commit and workflow. Consumers can verify the package came from the claimed repository. No configuration needed.

Gaps and limitations

The escape hatches exist and will be used. --dangerously-allow-all-scripts is a single flag that disables the entire script protection. Teams under deadline pressure will reach for it. Name-only approvals (--no-allow-scripts-pin) skip version pinning entirely. The protection is only as strong as the discipline to not bypass it.

The migration cost is real. Every project that depends on packages with postinstall scripts (native addons, build tools, node-gyp bindings) will hit a wall of pending approvals on first install with npm v12. The pressure to approve everything at once and move on is high. The security value comes from actually reviewing each script, which takes time most teams have not budgeted for.

Only three CI providers support OIDC. GitHub Actions, GitLab CI, and CircleCI, cloud-hosted runners only. Self-hosted runners are excluded. Teams on Jenkins, Buildkite, AWS CodeBuild, or self-hosted GitLab runners cannot use trusted publishing today and remain on long-lived tokens until more providers are supported.

Staged publishing cannot create new packages. You can only stage a publish for a package that already exists on the registry. The first publish of a new package still uses the traditional path. This means the initial publication, where supply-chain attackers register new names, is not covered by the staged workflow.

The deprecation timeline leaves a gap. Between now and January 2027, 2FA-bypass tokens can still publish directly. That is roughly six months where the old token model and the new one coexist. Attackers holding leaked tokens have a window before the old path closes.

Root package scripts still run. The new defaults block scripts from your dependencies, not your own project’s scripts. If an attacker gets code into your repository (through a compromised PR or a supply chain injection into your own postinstall), your own scripts still execute.

Node-gyp implicit builds are also blocked. Packages with a binding.gyp file and no explicit install script used to trigger node-gyp rebuild automatically. That implicit build is now blocked by the same allowScripts system. Correct, but it will surprise teams who did not know their dependencies compiled native code. Common packages like bcrypt, better-sqlite3, and node-sass will need explicit approval.

What to do now

If you consume packages. Upgrade to npm v12. Run npm approve-scripts --allow-scripts-pending on your projects and review each pending dependency before approving. Commit the allowScripts field to your package.json. Set --strict-allow-scripts in your .npmrc so unapproved scripts fail the build rather than warn. Do not use --dangerously-allow-all-scripts except as a temporary measure with a tracked plan to remove it.

If you publish packages. Set up trusted publishing (OIDC) with your CI provider before Phase 2 arrives in January 2027. If your CI provider is not supported, plan for staged publishing and a manual approval step. Rotate and revoke any existing 2FA-bypass tokens that are no longer needed.

Regardless. Keep ignore-scripts=true in .npmrc as a belt-and-suspenders defense. Use npm ci (not npm install) in CI to enforce the lockfile. Pin your dependencies with --save-exact. These practices were good before npm v12 and they remain good after.