# 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](https://www.stepsecurity.io/blog/subql-ecosystem-compromised).
- **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](https://www.stepsecurity.io/blog/tensorlake-npm-compromised-hostage-token-worm).
- **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](https://www.stepsecurity.io/blog/sckit-supply-chain-worm-hits-memtensor-npm-pypi-scopes).

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](https://unit42.paloaltonetworks.com/npm-supply-chain-attack/) 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](https://socket.dev/blog/axios-npm-package-compromised).

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](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack).

Your `package.json` might not change at all:

```json
{
  "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+**:

```ini
# .npmrc
min-release-age=7
```

The value is in **days**. This option was introduced in [npm 11.10.0](https://github.com/npm/cli/releases/tag/v11.10.0). If no available version meets the age requirement and the dependency constraints, installation fails. [Configuration reference](https://docs.npmjs.com/cli/v11/using-npm/config/#min-release-age).

For **pnpm 11+**:

```yaml
# 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](https://pnpm.io/settings/dependency-resolution#minimumreleaseagestrict).

For **Yarn 4.12+**:

```yaml
# .yarnrc.yml
npmMinimalAgeGate: "1w"
```

Yarn also enables a one-day delay by default. This increases it to a week. [Yarn documentation](https://yarnpkg.com/features/security#age-gate).

For **Bun 1.3+**:

```toml
# bunfig.toml
[install]
minimumReleaseAge = 604800
```

Bun uses **seconds**: `604800` is seven days. The setting was introduced in [Bun 1.3](https://bun.com/blog/bun-v1.3#minimum-release-age).

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](https://bun.com/docs/pm/cli/install#minimum-release-age).

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:

```ini
# .npmrc
ignore-scripts=true
```

This disables automatic script execution during installation. Explicit commands such as `npm run build` still work. [npm documentation](https://docs.npmjs.com/cli/v11/using-npm/config/#ignore-scripts).

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](https://github.com/npm/cli/releases/tag/v12.0.0).

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

```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:

```yaml
# 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](https://pnpm.io/settings/build).

For **Yarn 4**:

```yaml
# .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](https://yarnpkg.com/configuration/yarnrc#enableScripts).

For **Bun**, save the setting in the project configuration:

```toml
# bunfig.toml
[install]
ignoreScripts = true
```

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

```toml
# 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:

```json
{
  "trustedDependencies": []
}
```

You can add reviewed packages to that list. With `ignoreScripts = true`, scripts remain disabled regardless of which packages you trust. [Bun documentation](https://bun.com/docs/pm/lifecycle).

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](https://docs.npmjs.com/cli/v11/commands/npm-ci/), [pnpm](https://pnpm.io/cli/install#--frozen-lockfile), [Yarn](https://yarnpkg.com/cli/install#details), and [Bun](https://bun.com/docs/pm/cli/install#configuration).

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`:

```ini
save-exact=true
```

New dependencies will be saved without a version range. [npm documentation](https://docs.npmjs.com/cli/v11/using-npm/config/#save-exact).

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

```yaml
saveExact: true
```

[pnpm documentation](https://pnpm.io/configuring#local-project-configuration).

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

```yaml
defaultSemverRangePrefix: ""
```

The empty string disables the automatic addition of `^` or `~`. [Yarn documentation](https://yarnpkg.com/configuration/yarnrc#defaultSemverRangePrefix).

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

```toml
[install]
exact = true
```

[Bun documentation](https://bun.com/docs/runtime/bunfig#installexact).

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](https://docs.socket.dev/docs/socket-for-github), [Aikido Security](https://help.aikido.dev/code-scanning/scanning-practices/malware-detection-in-open-source-dependencies), 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](https://www.stepsecurity.io/blog/tensorlake-npm-compromised-hostage-token-worm).

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](https://blog.gitguardian.com/ghostaction-github-actions-supply-chain-attack-returns/).

## 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`**:**

```ini
# 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](https://docs.npmjs.com/cli/v11/using-npm/config/).

**pnpm 11+ —** `pnpm-workspace.yaml`**:**

```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](https://pnpm.io/settings/dependency-resolution#minimumreleaseage), [scripts](https://pnpm.io/settings/build#ignorescripts), and [saving versions](https://pnpm.io/configuring#local-project-configuration).

**Yarn 4.12+ —** `.yarnrc.yml`**:**

```yaml
# 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](https://yarnpkg.com/configuration/yarnrc).

**Bun 1.3+ —** `bunfig.toml`**:**

```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](https://bun.com/docs/pm/cli/install#minimum-release-age), [scripts and exact versions](https://bun.com/docs/runtime/bunfig).

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](https://docs.npmjs.com/cli/v11/commands/npm-ci/) | `package-lock.json` | `npm ci` |
| [pnpm](https://pnpm.io/cli/install#--frozen-lockfile) | `pnpm-lock.yaml` | `pnpm install --frozen-lockfile` |
| [Yarn](https://yarnpkg.com/cli/install#details) | `yarn.lock` | `yarn install --immutable` |
| [Bun](https://bun.com/docs/pm/cli/install#configuration) | `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**](https://app.reforma.ai/new). 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.
