How to Set Up Dependency Cooldowns in npm and pip to Block Fresh Malicious Packages
Learn to configure npm and pip dependency cooldowns so installs only trust package versions old enough to have survived the supply chain attack detection window.
A malicious npm or PyPI package usually does not stay malicious-looking for long. Attackers publish a poisoned release, automated build pipelines around the world pull it within minutes, and only later does someone notice the strange network calls or the stolen credentials and get the package pulled from the registry. By then the damage is already sitting in production. A dependency cooldown is a simple, config-only defense against exactly that timeline: you tell npm or pip to refuse any package version that has not been publicly available for some minimum amount of time, so the version reaching your machine has already survived the window where most malicious releases get caught and removed. This tutorial teaches you how to turn that on for both npm and pip, watch it actually block a fresh release, and safely override it when you genuinely need a package published five minutes ago.
Table Of Content
- Prerequisites
- How a Dependency Cooldown Actually Works
- Step 1: Confirm Your Toolchain Is New Enough
- Step 2: Understand Where These Settings Can Live
- npm’s three locations
- pip’s three locations, and the default that surprises people
- Step 3: Set Up an npm Cooldown
- Create a project and set the cooldown
- Watch it block a real, currently fresh release
- Override it for a genuine emergency
- The sibling option: an exact date instead of a day count
- Step 4: Set Up a pip Cooldown
- Create an isolated virtual environment and set the cooldown
- Watch it block a real, currently fresh release
- Override it for a genuine emergency
- Step 5: Add Vulnerability Scanning as a Second Layer
- Common Mistakes and Gotchas
- Verifying Everything End to End
- Next Steps
This build was done on Ubuntu 26.04 LTS with Node.js v24.18.0 and npm 11.16.0 for the npm half, and Python 3.11.15 with pip 26.2 for the pip half. Every command below, and every block and error message shown, was run for real against the live npm and PyPI registries while writing this tutorial, not copied from documentation.
Prerequisites
- A Linux machine (this tutorial uses Ubuntu; the commands are the same on any distribution, only installation of Node.js and Python differs).
- npm 11.10.0 or later. The
min-release-agesetting used in this tutorial does not exist in older npm releases. Runnpm --versionto check; if you are behind, install a current Node.js LTS from nodejs.org or via your package manager. - pip 26.1 or later. The
--uploaded-prior-toflag used in this tutorial only accepts a plain duration (likeP1Dfor one day) from pip 26.1 onward; pip 26.0 requires a full ISO 8601 datetime instead. This is a property of pip itself, not of your Python version, so any Python 3 interpreter works as long as its pip is current. Upgrade withpython3 -m pip install --upgrade pip. - Comfort running
npm installandpip installalready. This tutorial explains the cooldown mechanism in depth but assumes you know what a package manager does. - No root or sudo is required for the recommended project-scoped and virtual-environment-scoped setup used here. Root is only needed if you choose the machine-wide (global) scope covered later.
How a Dependency Cooldown Actually Works
Both npm and PyPI record an upload timestamp for every version of every package. A cooldown setting does not scan for malware or compare hashes against a blocklist; it does something much simpler and, because of that, much harder for an attacker to route around. When you run an install, the package manager computes a cutoff time (either “N days before right now” or an exact date you supply), then builds your dependency tree using only versions uploaded before that cutoff. A version published an hour ago simply does not exist as far as the resolver is concerned, in exactly the same way a version that was never published does not exist. If nothing satisfies your version requirement before the cutoff, the install fails loudly instead of silently grabbing the newest thing.
This matters because of how fast most supply-chain incidents get caught. Registries, security researchers, and automated scanners routinely spot and pull malicious packages within hours to a few days of publication. A cooldown of even 24 to 72 hours means your installs only ever see versions that have already survived that scrutiny window. It will not stop every attack, and it does nothing for a compromise that stays hidden longer than your cooldown, but it closes off the most common and most damaging pattern: a burst of automated installs in the first few minutes after a malicious publish.
Step 1: Confirm Your Toolchain Is New Enough
Check both version numbers before doing anything else, since an older npm or pip will simply reject the settings below as unrecognized:
node --version
npm --version
python3 --version
pip --version
This tutorial’s environment reported:
v24.18.0
11.16.0
Python 3.11.15
pip 26.2 from /path/to/venv/lib/python3.11/site-packages/pip (python 3.11)
Both comfortably clear the 11.10.0 and 26.1 minimums. If pip --version comes back lower than 26.1, run python3 -m pip install --upgrade pip first; everything past this point assumes the newer flag syntax.
Step 2: Understand Where These Settings Can Live
Before setting anything, it helps to know that both tools support the same basic idea: a setting can live at a scope narrower than “this whole machine, forever.” Getting the scope wrong is the single most common mistake with this feature, so it is worth teaching explicitly rather than skipping to the command.
npm’s three locations
npm’s config set command takes a --location flag with three values:
project, writes to a.npmrcfile in the current directory. Only affects installs run from that project.user, writes to your personal~/.npmrc. Applies to every project you run as that user, unless a project-level file overrides it.global, writes to npm’s global config file, typically requiring root. Applies machine-wide.
pip’s three locations, and the default that surprises people
pip’s config set command takes --global, --user, or --site flags with a similar meaning, --site being the per-virtual-environment equivalent of npm’s project scope. The gotcha: if you run pip config set without passing any of those three flags, pip does not default to the current virtual environment. Running pip config debug shows the real precedence order pip searches, and the documentation is explicit that a bare pip config set falls through to the user-level file whenever a virtual environment’s own config file does not already exist yet, which it usually does not on a fresh environment. In practice that means a bare pip config set global.uploaded-prior-to P1D run inside a fresh virtual environment quietly writes to your personal ~/.config/pip/pip.conf instead, a file that will keep affecting every other virtual environment and system-wide install you ever run as that user, not just the project you meant to scope it to. Always pass --site, --user, or --global explicitly and never rely on the default.
For learning purposes, this tutorial uses project scope for npm and --site scope for pip: both are isolated to one folder, both are safe to experiment with, and both are the closest match to how most people actually want this to behave, contained to a specific project rather than silently changing every install on the machine.
Step 3: Set Up an npm Cooldown
Create a project and set the cooldown
mkdir cooldown-demo && cd cooldown-demo
npm init -y
npm config set min-release-age 2 --location=project
This writes a single line to a new .npmrc file in the current directory:
min-release-age=2
min-release-age takes a whole number of days. Setting it to 2 means npm will only install versions that were published more than two days ago.
Watch it block a real, currently fresh release
npm itself ships new versions of the npm package constantly, which makes it a convenient, always-available way to see the cooldown do something real instead of a contrived example. At the time of writing, npm 11.19.0 had been published under a day earlier. With the two-day cooldown above active, installing it directly fails:
npm install [email protected]
npm error code ETARGET
npm error notarget No matching version found for [email protected] with a date before 7/28/2026, 2:53:56 AM.
npm error notarget In most cases you or one of your dependencies are requesting a package version that doesn't exist.
That is the cooldown working exactly as intended. npm computed “two days before right now,” found that version 11.19.0 was published after that cutoff, and refused to consider it at all, the same error you would get if the version genuinely did not exist.
Override it for a genuine emergency
Sometimes the version you need really was published an hour ago, for example a hotfix for a bug that is actively breaking your build. Rather than turning the whole cooldown off, override it for a single install with a CLI flag:
npm install [email protected] --min-release-age=0
added 1 package, and audited 145 packages in 2s
Setting --min-release-age=0 on the command line for that one invocation takes precedence over the project-level .npmrc, so the install proceeds normally. The persistent setting in .npmrc is untouched, so the very next plain npm install goes back to enforcing the two-day window. Confirm that by removing the package and reinstalling an older, already-aged version with no override at all:
rm -rf node_modules package-lock.json
npm install [email protected]
npm 11.18.0 was published about a month before this tutorial was written, well outside the two-day window, so it installs immediately with no error and no override needed, exactly the everyday case a cooldown is designed to leave alone.
The sibling option: an exact date instead of a day count
npm also has a before option that takes an exact date instead of a relative day count: npm install --before=2026-07-01 would only allow versions published on or before July 1, 2026. The two are meant to work together rather than compete: if both apply to the same source (say, both are set in the same .npmrc), the exact date in before wins over the relative min-release-age window, which makes sense since an explicit date is a more specific instruction than a rolling window. before is most useful for reproducing a build as it existed on a specific past date rather than for ongoing supply-chain protection, which is what min-release-age is for.
One npm detail worth being precise about: some published npm documentation references a companion min-release-age-exclude setting for exempting specific package names from the cooldown. Testing directly against the npm 11.16.0 CLI used in this tutorial, that setting does not exist; passing it returns min-release-age-exclude is not a valid npm option. If you need to install one specific package without a cooldown, use the --min-release-age=0 per-install override shown above, or check npm config list -l against your own installed npm version before relying on an exclude list.
Step 4: Set Up a pip Cooldown
Create an isolated virtual environment and set the cooldown
mkdir cooldown-demo-pip && cd cooldown-demo-pip
python3 -m venv venv
source venv/bin/activate
pip config --site set global.uploaded-prior-to P1D
Two things to notice here. First, --site is passed explicitly, for the reason covered in Step 2, writing the setting into this virtual environment’s own pip.conf rather than your personal user config. Second, the key is global.uploaded-prior-to: the global here is a section name inside pip’s config file meaning “apply to every pip subcommand,” not the --global scope flag; pip’s config file sections are named after the command they apply to (install, list, and so on), and global is the special section that applies everywhere. The value P1D is an ISO 8601 duration meaning one day.
This writes to venv/pip.conf:
[global]
uploaded-prior-to = P1D
Watch it block a real, currently fresh release
pip itself is a convenient live target for the same reason npm was: version 26.2 of the pip package on PyPI had been published under a day before this was written. With the one-day cooldown active:
pip install "pip==26.2"
ERROR: Could not find a version that satisfies the requirement pip==26.2 (from versions: 0.2, 0.2.1, [... dozens of older releases omitted here for length ...], 26.0, 26.0.1, 26.1, 26.1.1, 26.1.2)
ERROR: No matching distribution found for pip==26.2
The bracketed note above is this tutorial’s own editing, added only to shorten a version list going back to pip’s very first 2008 release; the real output lists every single one. What matters is what is missing from that list: it stops at 26.1.2, the last release older than the one-day cutoff. 26.2 is not merely filtered out of the list, it is treated as if it does not exist at all, matching npm’s behavior. This filtering only works against indexes that publish upload-time metadata alongside each release; PyPI’s own index does, so the default case works out of the box, but a private or mirrored index (Artifactory, Nexus, devpi, and similar) needs to be checked, since one that does not forward that metadata will silently let the cooldown do nothing rather than error.
Override it for a genuine emergency
pip install "pip==26.2" --uploaded-prior-to=P0D
Successfully installed pip-26.2
P0D means a zero-day minimum age, effectively “no cooldown, just for this one install.” As with npm, this only overrides the single command it is attached to; the persistent venv/pip.conf setting is unchanged, so the next plain pip install in this environment enforces the one-day window again.
Step 5: Add Vulnerability Scanning as a Second Layer
A cooldown protects you from installing something too recently published to have been vetted yet. It does nothing for a vulnerability that was always there, sitting in a package that has been stable and trusted for years. That is a different problem, solved by a different kind of tool: a vulnerability scanner that checks your actual installed dependency tree against a known-issues database.
npm has this built in:
npm audit
pip does not ship an equivalent, but the community-maintained pip-audit tool fills the same role. Install and run it inside your virtual environment:
pip install pip-audit
pip-audit
Running it in this tutorial’s own throwaway environment turned up a real, specific finding, useful as a sample of what the tool’s output looks like:
Found 2 known vulnerabilities in 1 package
Name Version ID Fix Versions
---------- ------- --------------- ------------
setuptools 79.0.1 PYSEC-2026-3447 83.0.0
A cooldown and an audit tool are complementary, not redundant: the cooldown buys time for the community to find problems in brand-new releases, and the audit tool catches problems that were already known about releases you may have installed long ago.
Common Mistakes and Gotchas
- Relying on pip’s default scope. As covered in Step 2, a bare
pip config setwith no--site,--user, or--globalflag writes to your personal user config far more often than people expect, quietly affecting every other project you touch. Always pass the scope flag explicitly. - Assuming
min-release-age-excludeexists. It is referenced in some npm documentation but does not work as an actual config key in the npm 11.16.0 CLI tested for this tutorial; use the--min-release-age=0per-install override instead. - Setting the cooldown, then having no documented override process. A cooldown that blocks a real production hotfix at 2 AM with no one on the team knowing the override flag is worse than not having one, since it turns a security control into an outage. Document
--min-release-age=0and--uploaded-prior-to=P0Dnext to wherever your team keeps its incident runbooks. - Assuming it works against any registry. Both mechanisms depend on the index serving upload-time metadata. Public npm and public PyPI do. An internal mirror might not, and the failure mode is silent (the cooldown simply does not filter anything) rather than an error, so it is worth testing against your actual internal registry, not just the public one, before trusting it in production.
- Forgetting this also delays your own team’s packages. If your organization publishes internal packages through the same registry, a machine-wide cooldown delays your own releases from reaching your own machines by the same window. That is sometimes desirable (it catches your own mistakes too) and sometimes not; decide on purpose rather than by accident.
Verifying Everything End to End
Confirm both settings are actually active before trusting them:
npm config get min-release-age --location=project
pip config list
The npm command should print 2 (or whatever number of days you chose), and pip config list should show global.uploaded-prior-to='P1D' sourced from your virtual environment’s pip.conf. Then repeat the core test from Steps 3 and 4 whenever you want to sanity-check a live environment: try installing a package version you know was published within your cooldown window and confirm it is refused, then add the override flag and confirm it succeeds. If both halves of that behave as expected, the cooldown is genuinely protecting installs from that environment, not just sitting inert in a config file.
Next Steps
A dependency cooldown is one layer in a broader supply-chain defense, not a complete one. From here, consider pairing it with a lockfile that pins exact, already-vetted versions so a cooldown is only ever consulted when you deliberately bump a dependency; this site’s walkthrough of pip’s pylock.toml format covers that for Python. For a container-based deployment, generating and signing a software bill of materials closes a related gap; see this site’s guide to SBOMs with Syft and Cosign. Whichever you add next, the goal is the same one this tutorial started with: make it structurally harder for a compromised dependency to reach a machine that matters before someone has had a chance to notice.








No Comment! Be the first one.