Skip to main content

NPM Is Totally Compromised. How Do You Run `npm install` Safely Now?

You rarely think about what code lands on your machine during a routine npm install, or what it might do once it gets there. You expect build errors or version conflicts. A compromised package can steal your SSH keys, GitHub tokens, AWS credentials, and access to crypto wallets—and then use your credentials to infect your own packages.

Updated 11 min readMarkdown
NPM Is Totally Compromised. 
How Do You Run `npm install` Safely Now?

A malicious release can turn up in a library you’ve used for years, caught in a chain of compromises spreading through other packages.

September and October 2026 alone gave us plenty of examples:

  • SubQuery. Researchers found credential theft and remote command execution code in @subql/common@5.8.3. The package could reach a project through other SubQuery dependencies. StepSecurity’s analysis.
  • Tensorlake. Version tensorlake@0.5.144 stole tokens, tried to spread to other packages, and installed a background process that deleted the user’s home directory if the stolen GitHub token was revoked. StepSecurity’s analysis.
  • MemTensor. Malicious releases of an AI plugin harvested credentials. During memory recall, the plugin also passed the user’s prompt to a hidden process. StepSecurity’s analysis.

Meanwhile, attacks from the Shai-Hulud family continued. Its approach is particularly unpleasant for package maintainers: the worm steals a publishing token, injects malicious code into packages the victim can publish, and releases new versions under their name. Unit 42 explains the mechanism and subsequent campaigns.

Earlier in 2026, even Axios was compromised. Malicious releases pulled in an additional dependency that installed a remote access trojan. Socket’s analysis.

TanStack packages were hit too. Compromised releases included code targeting CI credentials and a Git dependency that ran a malicious script through its prepare hook. Socket’s separate analysis.

Your package.json might not change at all:

{
  "dependencies": {
    "some-package": "^1.2.3"
  }
}

That range allows newer versions considered compatible under semver, such as 1.3.4. If the package manager selects a compromised release the next time it resolves dependencies, a routine installation becomes an entry point for an attack.

npm and other package managers need to rethink how we work with dependencies. In the meantime, there are several things you can configure to reduce the risk on your machines and in CI.

1. Give new releases a cooldown period

The first setting I’d enable is a delay before installing newly published releases. We’ll use a week in the examples: if a version was published less than seven days ago, leave it alone for now. Personally, I think three days would be enough too.

That gives researchers and maintainers time to spot a compromised release, warn users, and get it removed from the registry.

For npm 11.10.0+:

# .npmrc
min-release-age=7

The value is in days. This option was introduced in npm 11.10.0. If no available version meets the age requirement and the dependency constraints, installation fails. Configuration reference.

For pnpm 11+:

# pnpm-workspace.yaml
minimumReleaseAge: 10080
minimumReleaseAgeStrict: true

Here, the value is in minutes: 10080 is seven days. Setting minimumReleaseAgeStrict: true prevents pnpm from choosing a newer release even when no older version satisfies the dependency constraints.

pnpm 11 has a one-day delay by default, but without strict mode it can fall back to a release that is too new. Set the policy explicitly. pnpm documentation.

For Yarn 4.12+:

# .yarnrc.yml
npmMinimalAgeGate: "1w"

Yarn also enables a one-day delay by default. This increases it to a week. Yarn documentation.

For Bun 1.3+:

# bunfig.toml
[install]
minimumReleaseAge = 604800

Bun uses seconds: 604800 is seven days. The setting was introduced in Bun 1.3.

There’s a catch: Bun applies the filter when resolving new versions. Dependencies already recorded in bun.lock stay unchanged, so adding this setting won’t retroactively check your entire lockfile. How the filter works.

Waiting a week doesn’t make a package safe. It does reduce the chance of installing a malicious release immediately after publication, before anyone has raised the alarm.

An urgent security fix may need to go in sooner. Make that exception for a specific update after reviewing it.

2. Restrict automatic install scripts

Packages can run commands through preinstall, install, and postinstall. Some dependencies have legitimate reasons for doing this, such as building native modules or downloading binaries.

Those commands run with the permissions of the user who started the installation. If that user can access SSH keys, tokens in .npmrc, or secrets in environment variables, a script potentially can too.

For npm, you can disable automatic scripts entirely:

# .npmrc
ignore-scripts=true

This disables automatic script execution during installation. Explicit commands such as npm run build still work. npm documentation.

Starting with npm 12, dependency install scripts are blocked by default unless the allowScripts policy permits them. So the claim that npm always runs every package’s scripts no longer applies to newer versions. npm 12 release notes.

With pnpm 11+, third-party dependency scripts also require approval. You can configure exceptions in pnpm-workspace.yaml:

# pnpm-workspace.yaml
allowBuilds:
  esbuild: true
  core-js: false

This illustrates the syntax; approve a package only after reviewing its script. Dependencies that aren’t listed are treated as unreviewed. With the default settings, pnpm fails the installation if they require a build.

To disable installation scripts for both the project and its dependencies:

# pnpm-workspace.yaml
ignoreScripts: true

With this setting enabled, an allowBuilds entry won’t override the ban. Choose whether to disable all scripts or allow specific dependencies. pnpm documentation.

For Yarn 4:

# .yarnrc.yml
enableScripts: false

This has been the default since Yarn 4.14. The setting restricts scripts from third-party packages; scripts in your own workspaces still run. You can allow individual dependencies through dependenciesMeta. Yarn documentation.

For Bun, save the setting in the project configuration:

# bunfig.toml
[install]
ignoreScripts = true

If you’ve already added minimumReleaseAge, put both settings under the same [install] section:

# bunfig.toml
[install]
minimumReleaseAge = 604800
ignoreScripts = true

Bun restricts dependency scripts even without this setting, but it has a built-in list of trusted packages. Saying that Bun doesn’t run install scripts by default would therefore be inaccurate.

If you need selective approvals instead of a blanket ban, use trustedDependencies in package.json. An empty list removes trust from all dependencies, including the built-in exceptions:

{
  "trustedDependencies": []
}

You can add reviewed packages to that list. With ignoreScripts = true, scripts remain disabled regardless of which packages you trust. Bun documentation.

Some dependencies may stop installing or working after you change these settings. You’ll need to check why they require a script and decide whether to allow it.

Restricting install scripts covers only part of the problem. Malicious code can still run during a normal import, a build, or application execution. In the MemTensor incident, execution was built into the plugin’s normal operation.

3. Commit your lockfile and use it in CI

Keep the lockfile in the repository alongside your code. It records specific versions of both direct and transitive dependencies.

In CI, use the command for your package manager:

Package manager Lockfile Command
npm package-lock.json npm ci
pnpm pnpm-lock.yaml pnpm install --frozen-lockfile
Yarn 4 yarn.lock yarn install --immutable
Bun bun.lock bun ci

These commands install from the lockfile and fail if it needs to change to match package.json. See the documentation for npm, pnpm, Yarn, and Bun.

For Bun, bun ci is equivalent to bun install --frozen-lockfile. Make sure you commit bun.lock: a regular bun install doesn’t automatically enable frozen mode just because it’s running in CI.

You can also remove ^ and ~ from direct dependencies, but that doesn’t replace a lockfile. Libraries have their own dependencies.

A lockfile doesn’t check for malware either. If a compromised version is already in it, a reproducible installation will keep installing that version. Handle dependency updates in separate PRs so you can review which packages and versions are changing.

Building your project shouldn’t also decide that it’s time to update dependencies.

You can save exact versions by default, so adding a dependency writes "1.2.3" to package.json instead of "^1.2.3".

For npm, add this to .npmrc:

save-exact=true

New dependencies will be saved without a version range. npm documentation.

For pnpm 11+, add this to pnpm-workspace.yaml:

saveExact: true

pnpm documentation.

For Yarn 4, add this to .yarnrc.yml:

defaultSemverRangePrefix: ""

The empty string disables the automatic addition of ^ or ~. Yarn documentation.

For Bun, add exact to the existing [install] section in bunfig.toml:

[install]
exact = true

Bun documentation.

These settings change how new dependencies are saved. They won’t automatically rewrite existing entries containing ^ or ~.

Pinning fixes the version of a direct dependency, but that package’s own dependencies may still use ranges. You still need a lockfile and frozen installs in CI. Pinned versions can then be updated through separate PRs, with the changes reviewed before merging.

4. Check dependency updates for malware

npm audit is useful for finding known vulnerabilities, but don’t rely on it alone. A version published yesterday that steals tokens today may not have a CVE or an entry in a conventional vulnerability database yet.

Tools such as Socket, Aikido Security, and StepSecurity can check dependencies for malicious code and suspicious behavior. StepSecurity, for example, has separate checks for known compromised versions and releases that are too new, described in its Tensorlake analysis.

Run these checks on dependency update PRs before merging. That gives you a warning while you can still stop the update.

5. Keep secrets out of jobs that install dependencies

Look at the permissions and secrets available to the process running npm install. Does it need production credentials? Permission to publish packages? A token with write access to every repository?

The less access that process has, the less a malicious dependency can steal or change.

Separate builds from deployments, pass secrets only to the jobs that need them, and limit GITHUB_TOKEN permissions to the task at hand.

Pin third-party GitHub Actions to full commit SHAs too. A moving tag can start pointing to different code while your workflow file stays exactly the same.

Attacks through CI are already happening at scale. In October 2026, another GhostAction wave hit more than 30,000 GitHub repositories, injecting workflows designed to steal secrets. GitGuardian’s investigation.

Final configs

Save these settings explicitly in your project so installation behavior doesn’t depend on whichever defaults a package manager release happens to choose. These configs set a one-week delay, disable automatic install scripts, and save exact versions for new dependencies.

npm 11.10.0+ — .npmrc:

# Minimum release age: 7 days
min-release-age=7

# Don't run scripts during installation
ignore-scripts=true

# Save exact versions without ^ or ~
save-exact=true

npm documentation.

pnpm 11+ — pnpm-workspace.yaml:

# Minimum release age: 7 days, in minutes
minimumReleaseAge: 10080

# Fail if no eligible version is available
minimumReleaseAgeStrict: true

# Don't run scripts during installation
ignoreScripts: true

# Save exact versions without ^ or ~
saveExact: true

Configuration reference: release age, scripts, and saving versions.

Yarn 4.12+ — .yarnrc.yml:

# Minimum release age: 7 days
npmMinimalAgeGate: "1w"

# Don't run install scripts from third-party dependencies
enableScripts: false

# Save exact versions without ^ or ~
defaultSemverRangePrefix: ""

This doesn’t disable scripts in your own workspaces. Yarn documentation.

Bun 1.3+ — bunfig.toml:

[install]
# Minimum release age: 7 days, in seconds
minimumReleaseAge = 604800

# Don't run scripts during installation
ignoreScripts = true

# Save exact versions without ^ or ~
exact = true

Configuration reference: release age, scripts and exact versions.

The npm, pnpm, and Bun examples disable install scripts entirely. If a dependency needs a build step, configure selective approvals through your package manager instead of the blanket ban. Saving exact versions also won’t rewrite existing entries containing ^ or ~.

Commit the lockfile alongside the configuration, and use the corresponding command in CI:

Package manager Lockfile CI installation
npm package-lock.json npm ci
pnpm pnpm-lock.yaml pnpm install --frozen-lockfile
Yarn yarn.lock yarn install --immutable
Bun bun.lock bun ci

Malware checks and CI permissions need separate configuration. Keep dependency updates in their own PRs, and pass deployment secrets only to the job that performs the deployment.


Build your next website with Reforma. Create with AI and take full control with a powerful visual website builder. Design every layout, style every detail, and publish on your own domain.

Lockfile review is the step most teams still skip.