# A Two-Week npm Cooldown with min-release-age

> How to protect a Next.js and Expo repo from Shai-Hulud style npm attacks with min-release-age, blocked install scripts, and exact lockfile installs.

- Author: Curtis Miller
- Published: 2026-10-28
- Categories: javascript, security, how-to
- Canonical URL: https://millarian.com/posts/npm-two-week-cooldown-after-shai-hulud/

[The first post in this series](https://millarian.com/posts/why-i-wait-two-weeks-to-install-packages/) covers why I wait two weeks before installing any new release. npm has this built in: `min-release-age` won't install any version that was published less than 14 days ago. Here is the setup I put in a TypeScript monorepo with a Next.js site and an Expo app.

## Two things that may already protect you

Check these first.

- **npm 11.19 blocks dependency install scripts by default** unless you approve them. `npm install-scripts ls` lists the pending ones, and `npm install-scripts approve|deny <pkg>` records the decision. Across my projects only two packages had install scripts (a lint resolver's postinstall and `fsevents`), and everything worked without them.
- **Committed lockfiles.** CI installs exact versions, so a new poisoned release can't arrive unless someone updates.

## Add a cooldown to .npmrc

Put this in an `.npmrc` at the root of each project and commit it. Build services that run npm in your repo pick it up too (Vercel and EAS both do).

```ini
min-release-age=14
ignore-scripts=true
```

`min-release-age` (npm 11.10 and later) only installs versions published at least that many days ago. Older npm ignores the line, which is fine, because lockfile installs are exact anyway. `ignore-scripts=true` covers the older npm on build servers: dependency scripts are skipped, while your own `npm run` scripts still run (only pre/post hooks are skipped).

I saw the rule work the same day. Installing `@axe-core/playwright`, npm skipped 4.14.0 (published that day) and installed 4.13.0 on its own.

For an urgent security fix, override it on purpose for that one install and say so in the commit message:

```shell
npm install some-package@latest --min-release-age=0
```

## Record the script decisions in package.json

I denied both packages explicitly with `npm install-scripts deny`, so the decision lives in the repo and not only in one machine's npm version:

```json
{
  "allowScripts": {
    "fsevents": false,
    "unrs-resolver": false
  }
}
```

## Use npm ci on build servers

CI and hosting services generally don't have a package age setting of their own. They build from what you commit, so the rules have to live in committed files. Have every CI job and build server install with `npm ci`, which installs the exact lockfile and fails if `package.json` and the lockfile disagree, instead of quietly resolving something newer:

```shell
npm ci
```

Most services let you set the install command. On Vercel, for example, it goes in `vercel.json`:

```json
{
  "installCommand": "npm ci"
}
```

## Verify signatures

I added `npm audit signatures` to the full check script in every project. It verified all 1,611 packages.
