# The package manager settings that would have blocked the Axios attack

By Jag Reehal, 2026-04-07. Canonical: https://arrangeactassert.com/posts/the-package-manager-settings-that-would-have-blocked-the-axios-attack/

<!-- Excerpt Start -->

On the 31st of March 2026, attackers hijacked an npm maintainer account and published malicious versions of `axios` with a remote access trojan baked in. npm pulled the bad releases after about two or three hours, but that was enough. Anyone who ran `npm install axios` during that window could have installed the trojan. The article [Post Mortem: axios npm supply chain compromise](https://github.com/axios/axios/issues/10636) has all the details.

This kind of attack keeps happening and the playbook barely changes: compromise an account, push a malicious update, hope people install it before anyone notices, get removed a few hours later.

Every major package manager now lets you defend against this. In this post I'll show you the setup for npm, pnpm, Bun and Yarn.

<!-- Excerpt End -->

## Why the Axios attack worked

The malicious versions were published as ordinary patch releases. If your project depended on `"axios": "^1.14.0"` and performed a fresh resolution without a lockfile pinning a safe version, install could resolve to the poisoned version automatically. And if the attacker included a `postinstall` script, npm ran it during installation without asking.

So one `npm install` could download the compromised package, run arbitrary code on your machine or CI runner, and exfiltrate credentials, tokens, SSH keys, source code - whatever it wanted.

The whole thing hinges on two assumptions: you install the bad version quickly, and npm runs the install script. The three settings break both.

### 1. Delay new releases

```ini
min-release-age=7
```

This tells npm to skip any version published in the last 7 days.

Seven days sounds like a lot. But most malicious releases get caught within hours. The Axios compromise was live for roughly 2-3 hours. With this setting, npm would have refused to resolve those versions until they were a week old, by which point they'd already been yanked.

Every major package manager has an equivalent, though the config key and units differ. npm calls this setting `min-release-age` (in days); pnpm and Bun call it `minimumReleaseAge` (minutes and seconds respectively); Yarn uses `npmMinimalAgeGate` (human-readable durations).

```yaml
# pnpm-workspace.yaml (minutes, so 10080 = 7 days)
minimumReleaseAge: 10080
```

```toml
# bunfig.toml (seconds, so 604800 = 7 days)
[install]
minimumReleaseAge = 604800
```

```yaml
# .yarnrc.yml
npmMinimalAgeGate: '7d'
```

### 2. Disable install scripts globally

```ini
ignore-scripts=true
```

npm packages can define lifecycle hooks (`preinstall`, `install`, `postinstall`) and by default npm runs them automatically. Attackers exploit this because their code executes during installation. You don't even need to import the package.

With `ignore-scripts=true`, npm downloads the package but refuses to run any install-time code. The install-time RAT payload described in the Axios compromise would not have executed during install.

## The tradeoff

Some legitimate packages need install scripts, especially native modules: `sharp`, `bcrypt`, `sqlite3`, `esbuild`, `puppeteer`. If something breaks, rebuild only the packages you trust:

```bash
npm ci --ignore-scripts
npm rebuild sharp esbuild
```

You can also override per project if you need to:

```ini
# project/.npmrc
ignore-scripts=false
```

### 3. Pin exact versions

```ini
save-exact=true
```

By default npm saves dependencies with a caret (`^1.14.0`), which means future patch and minor versions get pulled in automatically. That's the convenience that made the Axios attack possible.

With `save-exact=true`, npm records `"axios": "1.14.0"`. No range, no floating. You still update dependencies, but through a PR or lockfile update. Not because someone pushed a release five minutes ago.

## Setup for your package manager

Pick yours, copy the config, and you are done.

### npm

Requires npm v11.10.0 or later. To make sure your team is on a compatible version, add `engines` to your `package.json` and set `engine-strict=true` in `.npmrc`.

```ini
# ~/.npmrc (or project .npmrc)
ignore-scripts=true
min-release-age=7
save-exact=true
engine-strict=true
```

### pnpm

`minimumReleaseAge` uses minutes. 10080 = 7 days.

```yaml
# pnpm-workspace.yaml
minimumReleaseAge: 10080
onlyBuiltDependencies:
  - sharp
  - esbuild
```

pnpm can block arbitrary install scripts using `onlyBuiltDependencies`. Once configured, only the listed packages may run build/install scripts. Without it, lifecycle scripts still run normally.

### Bun

`minimumReleaseAge` uses seconds. 604800 = 7 days. Bun is more restrictive by default: it does not run arbitrary dependency lifecycle scripts, uses a built-in allowlist for many common packages, and lets you allow additional ones with `trustedDependencies` in `package.json`. If you want to disable lifecycle scripts entirely, use `bun install --ignore-scripts`.

```toml
# bunfig.toml
[install]
minimumReleaseAge = 604800
```

```json
// package.json
{
  "trustedDependencies": ["sharp", "esbuild"]
}
```

### Yarn (current releases)

`npmMinimalAgeGate` accepts human-readable durations. Disable scripts globally and allow them per package with `dependenciesMeta`.

```yaml
# .yarnrc.yml
npmMinimalAgeGate: '7d'
enableScripts: false
dependenciesMeta:
  sharp:
    built: true
  esbuild:
    built: true
```

## Lockfiles and CI

Even with these settings, commit your lockfile. A committed `package-lock.json` (or `pnpm-lock.yaml`, `bun.lock`, `yarn.lock`) means CI gets the same dependency graph every time, fresh installs don't resolve to something unexpected, and dependency updates show up in PRs where you can actually review them.

In CI, always use the lockfile-only install command for your package manager:

```bash
npm ci              # not npm install
pnpm install --frozen-lockfile
bun install --frozen-lockfile
yarn install --immutable
```

`npm install` in CI can silently update the lockfile and pull in new versions. `npm ci` refuses to do that. The other managers behave the same way with their respective flags.

Commit the lockfile. Review lockfile diffs. Update dependencies on purpose. Don't let CI grab whatever happens to be newest.

## Why this combination works

The Axios attack needed three things to go right: the victim installs quickly, the dependency range floats to the bad version, and install scripts run automatically.

| What the attacker needs               | What blocks it                                       |
| ------------------------------------- | ---------------------------------------------------- |
| Install before the package is noticed | `min-release-age=7`                                  |
| Auto-upgrade to the newest release    | `save-exact=true`                                    |
| Execute malware during install        | Disable install scripts / allowlist trusted packages |

Most supply chain attacks follow the same pattern. These settings won't stop everything, but they would have stopped this one, and they will probably stop the next few too.

## Stay safe out there

Scroll up to the [Setup for your package manager](#setup-for-your-package-manager), pick your package manager, and paste the config.

Commit your lockfiles, use lockfile-only installs in CI, review dependency changes in PRs, and keep an allowlist for packages that genuinely need install scripts.