A dependency cooldown tells a package manager not to select registry releases younger than a set age. A three-day window can give maintainers and security researchers time to identify a compromised npm or PyPI release before it enters your project. It is not a malware scanner, it cannot protect against malicious versions already approved or locked, and it can delay urgent security patches. Put the setting in the repository, commit a lockfile and define a reviewed emergency-update path.
A malicious package does not have to wait for your application to run. In npm, installation can trigger it. In Python, building, importing or starting the interpreter can trigger it. This depends on how the package was tampered with. By the time someone notices, a developer laptop or CI runner may already have handed over credentials. So can we avoid installing a release during its riskiest first few days?
We recommend a three-day release-age gate for registry packages, backed by a committed lockfile and a process for urgent security updates. In this article, we explain why, show where to configure it, and point to the gaps it cannot close. Also, we have published an auditable supply-chain skill that checks whether the settings are in place.
What changed in npm and PyPI threats?
Shai-Hulud first appeared in September 2025. Then it returned in later npm campaigns. Its November 2025 wave affected 796 unique npm packages. Other npm and PyPI compromises followed in 2026. This includes malicious Lightning and Microsoft durabletask releases.
Now, a fresh version can move from a registry into a real development environment before a security team has had time to assess it. This is the narrow window a cooldown is designed to address.
A self-replicating worm has been moving through package registries such as npm and PyPI, hitting unsuspecting developers and exfiltrating credentials: CI/CD secrets, API keys, and tokens for OpenAI, GitHub, Microsoft, SAP, Red Hat, Bitwarden and Mistral AI. The worm’s moniker is ‘Shai-Hulud’, like the giant sand worms from Dune.

The first two waves were followed by other attacks in April and May 2026, leading up to the Megalodon attack and the most recent Shai-Hulud on 4 August 2026. In just under a year, this attack went mainstream. Reading through the attacks shows how fast exfiltration happens once the package manager downloads a compromised package.
These attacks are highly effective. So we expect more of them.
The fix itself is easy to apply. But it is buried in the documentation of the package managers: set a minimum age before a version is eligible for installation.
The specifics differ per manager and it can be a hassle to track down all the right steps. So we have summarised the gotcha’s, do’s and don’ts into a skill, which we are releasing together with this article.
When can a compromised package execute code?
Shai-Hulud is so effective because compromised packages run their malicious code as soon as the package is pulled in.
npm packages can execute lifecycle scripts such as preinstall and postinstall during installation, before your application imports them. A pipeline that scans after installing dependencies may therefore run too late. A package that fails at a later stage of installation may still have executed an earlier lifecycle script. Script execution depends on the package and the package manager’s settings; it is not true of every npm download. See npm’s script documentation.
On npm, the payload can run during several hooks such as preinstall and postinstall. preinstall runs before installation completes. It even runs when the install fails, but does not have access to the unpacked tarball. postinstall runs with npm ci, and has access to the unpacked tarball. So it can load additional malicious files. A scanner used by CI/CD pipelines is already too late, because the payload had run first using either of these hooks.
On PyPI, it does not even need an install hook. Starting python is enough. A *-setup.pth file dropped into site-packages executes at Python interpreter startup and runs the payload from there. Another way to backdoor packages is to add malicious code to __init__.py. This was done for backdoored versions of Lightning (2.6.2 and 2.6.3), published on 30 April 2026.
This also means that a wheels-only policy does not help protect against these backdoored packages. Importing the package executed start.py out of a hidden _runtime directory and ran an eleven-megabyte obfuscated JavaScript stealer called router_runtime.js. Snyk flagged it about eighteen minutes after publication, and traced the same payload to the npm campaign of the day before.

None of the attacks we looked at started with a clever exploit. The chalk and debug maintainer lost their account to a phishing mail spoofing npmjsa.]help, which collected a username, a password and a live TOTP code, allowing the attackers full access to the account. These packages have thousands of daily downloads. It makes them a juicy target.
Once the payload ran, it exfiltrated credentials. Microsoft’s durabletask is the clearest published example we have. Three malicious versions were published on 19 May 2026, carrying fourteen lines of injected Python that ran at import time, activated only on Linux, and dropped a 28KB payload into /tmp.
Microsoft pulled the packages the same day. The attack window was just a little bit over half an hour.
StepSecurity took the payload apart, and showed what the second stage could exfiltrate:
- Cloud credentials for AWS, Azure and GCP
- SSH keys, Git credentials, Docker configuration
- npm, PyPI and Cargo tokens
- Shell histories and
.envfiles - Password managers, including 1Password, Bitwarden,
passandgopass - Configuration for AI coding tools: Claude Desktop, Cursor’s MCP config, VS Code settings, Zed, Codeium, Continue
The filesystem collector worked from a list of 99 hardcoded credential paths, on laptops and CI runners, and the secrets were gone before anyone was even aware of the attack.
Another issue is removing the artifacts after being hit with compromised packages. Several waves of Shai-Hulud modified Claude Code hooks and VS Code auto-run tasks to gain persistence, leaving router_runtime.js and setup.mjs behind in .claude/ and .vscode/, where they survive npm uninstall. One variant ran a gh-token-monitor daemon that polled every sixty seconds and fired rm -rf ~/ when it detected that a token had been revoked.
Still, we’d argue that revoking and rotating the credentials and secrets should always have priority. A laptop can be replaced.
This raises a question we didn’t have to ask two years ago. Who is running npm install on your machine?
Why do coding agents make the issue more urgent?
For better or worse, LLM coding agents have taken off in the past two years. And dependencies now get added by LLM coding agents when performing their tasks.
A coding agent can add a dependency even when no one has asked it to install a package. It decides the package will help complete the task, then asks the package manager to find a compatible version. Without a release-age rule, this could be a version published only minutes ago. If the release is malicious and its code runs during installation or use, the developer’s machine or CI runner may be compromised before anyone has reviewed the new dependency.
The malware authors use these LLM coding agents in their attacks: durabletask enumerated Claude Desktop, Cursor, VS Code, Zed, Codeium and Continue. AI tooling is a collection target and a persistence location, as the .claude/ artifacts above show.
Can’t we just tell the model to be careful? Sure, but instructions live in a context window. They can be summarised away, buried under forty turns of conversation, contradicted by a later request, or simply not followed. On the other hand, a package manager configuration is a hard technical constraint. The agent inherits it without being told it exists, and it cannot be talked out of it. The model does not need to understand supply chain security. It just has to be unable to install the compromised version in the first place.
What does a three-day dependency cooldown do?
Luckily, there is an easy fix. Configure your package manager to refuse any version published less than three days ago.
This prevents installation of malicious packages by both humans and agents. There is no signature list to maintain and no scanner that has to stay ahead of the attacker. The constraint is in the tool itself.
A cooldown works because compromised package gets detected before it can be installed. The latest version is held back. Much of the detection pipeline is automated, because registries and security vendors scan everything that gets published. For instance, the npm registry is heavily monitored by security firms such as Wiz. The rest of the detection can come from the compromised maintainers themselves, who find out that their account had published something they did not write.
Waiting a few days before installing a new package costs very little.
What is the cost? Security fixes may be delayed
Almost no package has to be from the bleeding edge, except security patches, which are unfortunately also held back by a cooldown. Npm’s own documentation says so: when the window stops npm audit fix from installing a patched version because the fix went out too recently, npm keeps the package at its vulnerable version.
We decided on a three-day cooldown period to make sure that there is proper time to intervene, so despite weekend and/or holidays there’s still enough time for detection in case the automated scanners miss something. The detection times of the incidents are all short. Lightning was flagged in about eighteen minutes. Chalk and debug were live for roughly two hours on 8 September 2025. The Shai-Hulud 2.0 packages were unpublished across a single morning on 24 November 2025. In most cases, one day was enough to identify and remove the malicious packages.
A cooldown of just one day already handles most, if not all, of the attacks. The extra days are insurance against the next campaign’s detection time rather than the last one’s, and they are cheap because the only difference is that delayed patch.
This is becoming an industry standard
pnpm shipped a one-day cooldown as the default in version 11, in late April 2026. If you are on pnpm 11, you already have a day of cooldown without configuring anything.
GitHub made a three-day cooldown the Dependabot default in July 2026, reasoning that new releases are a common supply chain entry point where a compromised version reaches your updates before the community has caught it. OpenAI reached for minimumReleaseAge after the Axios compromise.
Other package managers followed suit. uv, npm and Poetry all have a minimum release age in some form or another but it is not on by default.
The delayed security patch
Dependabot took an interesting route. Its cooldown applies to version updates only, and security updates are still applied immediately, so a critical fix is never delayed. It can make that distinction because it knows which updates are security updates. A package manager resolving a dependency graph does not, because a patched version and a poisoned version look identical to npm install.
So at the package manager level, the tool is per-package exemption. Every manager we checked has one: min-release-age-exclude on npm (names or minimatch globs), minimumReleaseAgeExclude on pnpm, exclude-newer-package in uv, and solver.min-release-age-exclude in Poetry.
Keep these lists short, and keep them to packages you publish yourself. This way, your critical patches of things you develop in-house still get applied right away.
Why you also need a lockfile
It is possible to configure cooldowns globally so they passively protect every invocation of pip or uv, by both humans and LLM coding agents.
There is another issue, however. A cooldown filters candidate versions at resolution time only.
Resolution happens wherever somebody or something runs an install, so the setting has to be configured for both developer laptops and CI runners. Your laptop may read the global ~/.npmrc or ~/.config/uv/uv.toml but a fresh CI runner has no home directory and reads neither, so unless the setting is committed to the repo, CI resolves the dependencies regardless of how carefully you configured your own machine and still gets compromised.
This is why we need a lockfile instead of globally configured cooldowns.
If CI installs from a committed lockfile with --frozen-lockfile or --immutable, it does not have to resolve anything at all. It installs the exact versions you already chose, based on the configured cooldown policy. In this sense, the lockfile acts like a contract, the terms of which are enforced by the package manager.
The cooldown applies to everything that needs to be imported, not only the packages you directly import. This is significant. For instance, take the Axios compromise. The compromised code was not in axios itself but in plain-crypto-js, a dependency Axios acquired in a patch release. chalk and debug were transitive for very nearly everyone who was exposed. We had Claude Opus read the documentation for each package manager and npm, Yarn, bun, uv, pip and Poetry all filter candidate versions while they work out the tree, which means the cooldown filter applies to transitive dependencies as well.
Where does a cooldown not help?
Cooldowns are a registry control and they cover registries only.
The Megalodon attack is the clearest example. A cooldown would have done nothing about it, because Megalodon did not arrive through npm install. It arrived as commits: forged CI bot identities pushing to 5,561 repositories, backdooring CI/CD workflows to exfiltrate cloud credentials, SSH keys, OIDC tokens and source-code secrets. There is no publication delay to apply to a commit.
Megalodon did not need a registry because it already had credentials, and those came out of an earlier compromise that did go through a package install. The npm and PyPI waves were the collection phase that made Megalodon possible.
Another form of the compromised dependency attack was documented by StepSecurity. They reported a compromise of the tj-actions/changed-files GitHub Action, which allowed the attackers to retroactively inject code into an existing commit, causing GitHub runners to pull in the compromised version that then exfiltrated, you guessed it, secrets and tokens.
Our supplychain skill, which we are publishing alongside this article, has a ci-audit function designed to protect GitHub Action CI pipelines against this type of attack.
How do you configure a three-day cooldown?
The specifics differ per manager:
.npmrc min-release-age=3 # DAYSpnpm config.yaml minimumReleaseAge: 4320 # MINUTES (default 1440 in v11).yarnrc.yml npmMinimalAgeGate: "3d" # duration stringbunfig.toml minimumReleaseAge = 259200 # SECONDSpyproject.toml [tool.uv] exclude-newer = "3 days" # durationpyproject.toml [tool.poetry] solver.min-release-age = 3 # DAYS, Poetry 2.4.0+pip.conf [install] uploaded-prior-to = P3D # or PIP_UPLOADED_PRIOR_TO=P3D
Four different units across seven tools, which is where this goes wrong in practice. Write 3 where you think it means day and the package manager wanted seconds and you’ve bought yourself a three-second cooldown, and nothing will tell you. This is the exact reason why we compressed all the gotcha’s, do’s and don’ts into a SKILL.md file to be used with coding agents.
Can the Eye Research skill check your setup?
We have built three skills that audit package manager configuration: supplychain:npm-harden for npm, pnpm and Yarn, supplychain:pypi-harden for uv, pip, Poetry and pdm, and supplychain:ci-audit for GitHub Actions. These check configuration but they do not detect active compromise or scan package contents.
Here is a run against one of our own internal services:
/supplychain:pypi-harden · baws-slack-bot (uv 0.11.2)🚨 1 critical ⚠️ 3 hardening ✅ 3 passing→ Add exclude-newer = "3 days" to [tool.uv] — closes the only gap that would have stopped a poisoned wheel📖 Hardening guide: https://docs.astral.sh/uv/concepts/resolution/🚨 Release-age gate 🚨 exclude-newer release age: 0d (uv) — every newly published version installs immediately. e.g. lightning 2.6.2/2.6.3 (Apr 2026) shipped a malicious wheel that runs `_runtime/start.py` on import, harvesting GitHub/npm/cloud credentials and worming through repos via planted `.claude/router_runtime.js` files. Socket detected and quarantined within hours; a 3-day exclude-newer window would have prevented installation entirely. └─ pyproject.toml: [tool.uv] exclude-newer = "3 days"⚠️ Install-time code execution policy ⚠️ sdist policy no `no-build` in uv config — `setup.py` executes during resolution on the `uv sync` developer path (the Terraform layer build already passes `--only-binary=:all:`). └─ pyproject.toml: [tool.uv] no-build = true⚠️ Tool version pinning ⚠️ uv version no uv version constraint — CI and developer machines may run different uv versions with different default settings. └─ pyproject.toml: [tool.uv] required-version = ">=0.11" ✅ requires-python = ">=3.14" declared, pinned via `.python-version` (3.14)⚠️ Version ranges ⚠️ lockfile enforcement `uv sync` in the Makefile omits `--frozen`, so a pyproject edit silently re-resolves the `~=` / `>=` ranges against the live index. └─ Makefile: uv sync --frozen...snip...
The skill allowed the LLM coding agent to enumerate the project’s settings. It immediately flagged the lack of a minimum release-age setting. So this would be the first thing to fix (which is just a prompt away).
If you like this set of skills but see ways to improve: we welcome all feedback and suggestions. Feel free to open an issue or pull request.
The joke is not lost on us. We are asking you to install a plugin into .claude/, in a post where we just discussed malware persisting through .claude/router_runtime.js. The skills use read-only bash (grep, for the most part), every check ships as plain text you can read before you run it, and the whole thing is a few hundred lines. Apply the rest of this post to us as well, and wait a few days before installing.
What can you do today?
- Set a release-age configuration (at user level, globally, is a good place to start), in the units your package manager expects, then commit the project-level equivalent so your teammates and CI inherit it.
- Commit your lockfile, and install from it in CI with
--frozen-lockfileor--immutable. - And accept that some version of this will reach a developer machine sooner or later. Assume breach, and make sure you have the tooling and the people to detect and respond to it 24/7.
Phishing, pull_request_target, Actions cache poisoning, a compromised scanner action, a .pth file, a native build hook, an editor extension. Each wave arrives somewhere the last one did not, and writing a control for each entry point is a game that can’t be won.
Every one of these attacks needs you to install a compromised version before anyone notices. The cooldown is what buys you just enough time to dodge the compromise.
Quick answers
Does a three-day cooldown stop every malicious npm or PyPI package?
It delays eligible registry releases during resolution. It cannot identify malware, block a malicious version that remains available after the window, or protect a project that explicitly installs a version outside the policy.
Does it block security updates too?
It can. Review urgent updates individually and use a narrow, documented exception if the risk of waiting is higher. Dependency update bots and package managers do not necessarily use the same exemption rules.
Does a lockfile replace the cooldown?
Enforce age when selecting new versions, then use a reviewed, committed lockfile for repeatable installs. A lockfile containing a malicious version is not made safe by freezing it.
Does this protect against malicious GitHub Actions or Git dependencies?
A registry publication-age filter does not govern repository commits or all non-registry package sources. Secure those paths separately.
Editor’s note: Eye Research conducted and directed the analysis in this article. AI tools were used in a supporting capacity during drafting and editing. Eye Research has reviewed the technical claims and supporting sources and has performed extensive editing to reach the final text. Eye Security retains editorial control and assumes editorial responsibility for the article as published. All conclusions are ours.
About Eye Security
We are a European cybersecurity company focused on 24/7 threat monitoring, incident response, and cyber insurance. Our research team performs proactive scans and threat intelligence operations across the region to defend our customers and their supply chains.
Learn more at https://eye.security/ and follow us on LinkedIn to help us spread the word.