Three Days of Patience: Eye Security’s Cooldown Rule for npm and PyPI, and Why Your Coding Agent Needs It Most
On 15 October 2026, Eye Security researchers Bas van den Berg and Jos Huiting published a recommendation of unusual concreteness: configure your package managers to refuse any registry release younger than three days. Not a scanner, not a policy document — a release-age gate, set in the tool itself, committed to the repository next to a lockfile, with a reviewed emergency-update path for the patches the gate will also delay. The argument is built on a year of Shai-Hulud waves and reads, in places, like a post-mortem of everything this site has covered since spring: the first Shai-Hulud appearance in September 2025, the November 2025 wave across 796 npm packages, the April and May 2026 campaigns, Megalodon, and the most recent Shai-Hulud on 4 August 2026. In just under a year, Eye writes, the attack went mainstream — and the window it exploits is the first few days of a release’s life, before scanners, vendors, and compromised maintainers have had time to react.
The mechanism section is the part worth your time, because it explains why post-install scanning is structurally too late. On npm, preinstall runs before installation completes — even when the install fails, though without access to the unpacked tarball — and postinstall runs under npm ci with the tarball available to load further malicious files. A CI scanner that runs after dependency installation is examining a host the payload has already owned. On PyPI it is worse: no install hook is needed at all. A *-setup.pth file dropped into site-packages executes at interpreter startup, and a backdoored __init__.py runs on import — which is how the Lightning 2.6.2 and 2.6.3 releases of 30 April 2026 worked (previously covered here), importing a start.py out of a hidden _runtime directory and running an eleven-megabyte obfuscated JavaScript stealer. Snyk flagged Lightning within about eighteen minutes and traced the payload to the previous day’s npm campaign — fast, and still too slow for anyone who installed in between. None of these waves, Eye stresses, started with a clever exploit: the chalk and debug maintainer lost the account to a phishing mail spoofing an npm help address that harvested username, password, and a live TOTP code.
The clearest published example, and what it enumerated
Microsoft’s durabletask (covered here in May) is Eye’s exhibit A: three malicious versions published 19 May 2026, fourteen injected lines running at import time, Linux-only, dropping a 28 KB payload into /tmp — pulled the same day, an attack window of barely half an hour. StepSecurity’s teardown showed what the second stage could take: cloud credentials for AWS, Azure, and GCP, SSH keys, git credentials, Docker config, npm, PyPI, and Cargo tokens, shell histories, .env files, password managers including 1Password, Bitwarden, pass, and gopass — and configuration for AI coding tools: Claude Desktop, Cursor’s MCP config, VS Code settings, Zed, Codeium, Continue. Ninety-nine hard-coded credential paths, harvested from laptops and CI runners before anyone knew the attack existed. Then the persistence sting: several Shai-Hulud waves modified Claude Code hooks and VS Code auto-run tasks, leaving router_runtime.js and setup.mjs behind in .claude/ and .vscode/ where they survive npm uninstall — one variant even ran a gh-token-monitor daemon polling every sixty seconds and firing rm -rf ~/ when it detected a revoked token. Eye’s priority order is blunt: revoke and rotate first; a laptop can be replaced. (Readers of our Warden briefing will recognise the .claude/ directory as contested ground by now.)
Why coding agents make a soft problem hard
The article’s sharpest paragraph is about who runs npm install on your machine in 2026. LLM coding agents add dependencies while completing tasks — sometimes without being asked to install anything, deciding a package will help and asking the resolver for a compatible version, which without an age rule can be a version published minutes ago. Eye’s answer to “can’t we just tell the model to be careful” deserves quoting in full: instructions live in a context window, where they can be summarised away, buried under forty turns, contradicted by a later request, or simply not followed — while a package-manager configuration is a hard technical constraint the agent inherits without being told and cannot be talked out of. The model never needs to understand supply-chain security; it just has to be unable to install the poisoned version. That framing travels well beyond this one control: anywhere an agent acts through a tool, prefer the constraint in the tool over the sentence in the prompt.
The idea is already winning — unevenly
Eye chose three days deliberately: the observed detection times are all short (Lightning in minutes, chalk/debug in roughly two hours, the Shai-Hulud 2.0 packages unpublished within a single November morning), so one day would have caught nearly everything — the extra two are insurance against the next campaign’s detection time, priced against weekends and holidays, and cheap because the only real cost is delayed patches. The industry is converging from different directions: pnpm shipped a one-day cooldown as the default in v11 (late April 2026); GitHub made three days the Dependabot default in July 2026, reasoning that fresh releases are a common supply-chain entry point; OpenAI reached for minimumReleaseAge after the Axios compromise; uv, npm, and Poetry all carry a minimum-age setting in some form, off by default. The delayed-patch catch is real — npm’s own docs admit the window can hold a package at its vulnerable version when the fix is too fresh — and Dependabot’s answer does not transfer: it can exempt security updates because it knows which updates are security updates, while a resolver cannot tell a patched version from a poisoned one. At package-manager level the tool is per-package exemption (min-release-age-exclude on npm, minimumReleaseAgeExclude on pnpm, exclude-newer-package in uv, solver.min-release-age-exclude in Poetry) — keep the lists short and limited to packages you publish yourself, so your own emergency patches still land immediately.
The lockfile is the contract; the registry is the boundary
Two structural points round out the piece. First, global configuration does not reach CI: a laptop reads ~/.npmrc or ~/.config/uv/uv.toml, but a fresh runner has no home directory and resolves against the live index regardless of your careful laptop — so the setting must be committed to the repo and CI must install from a committed lockfile with --frozen-lockfile or --immutable, installing exactly the versions already chosen under the cooldown policy. The cooldown also covers transitive dependencies, not just direct ones — Eye had Claude Opus verify across the npm, Yarn, bun, uv, pip, and Poetry docs that age filtering applies during tree resolution — which matters because the poison is so often transitive: the Axios compromise lived in plain-crypto-js, a dependency Axios acquired in a patch release, and chalk and debug reached nearly everyone transitively. Second, the registry is the whole of this control’s jurisdiction. Megalodon (covered here in May) arrived as commits from forged CI-bot identities across 5,561 repositories, backdooring workflows directly — no publication delay applies to a commit — and StepSecurity’s tj-actions/changed-files case showed attackers retroactively injecting code into an existing commit. A cooldown would have done nothing about either; the npm and PyPI waves were the collection phase that made Megalodon possible.
How to configure it, and the units trap
The per-manager settings, as Eye documents them: min-release-age=3 in .npmrc (days); minimumReleaseAge: 4320 in pnpm’s config.yaml (minutes — default 1440 in v11); npmMinimalAgeGate: "3d" in .yarnrc.yml (duration string); minimumReleaseAge = 259200 in bunfig.toml (seconds); exclude-newer = "3 days" under [tool.uv] in pyproject.toml; min-release-age = 3 under [solver] in poetry.toml (days, Poetry 2.4.0+); uploaded-prior-to = P3D in pip.conf. Four different units across seven tools — write 3 where the tool wanted seconds and you have bought yourself a three-second cooldown that nothing will warn you about. That trap is Eye’s stated reason for publishing machine-readable skills alongside the article: supplychain:npm-harden, supplychain:pypi-harden, and supplychain:ci-audit for GitHub Actions, which audit configuration (not active compromise, not package contents — the scope is explicit). The sample run against Eye’s own baws-slack-bot service is honest advertising: one critical (no age gate), three hardening notes, three passing — with the fix one prompt away. And the authors land the joke before anyone else can: yes, they are asking you to install a plugin into .claude/ in a post about malware persisting through .claude/router_runtime.js — read-only bash, plaintext checks, a few hundred lines, read-it-before-you-run-it, and, quote, “apply the rest of this post to us as well, and wait a few days before installing.” The closing line is the thesis: “The cooldown is what buys you just enough time to dodge the compromise.” Transparency note, from Eye’s own editor’s box: AI tools supported drafting and editing; Eye reviewed the technical claims, edited extensively, and retains editorial responsibility. We report that as stated.
What to do today
- Set the age gate in the units your manager expects, then commit the project-level equivalent so teammates, agents, and CI inherit it — user-level config alone leaves every fresh runner unprotected.
- Commit your lockfile and enforce it in CI (
--frozen-lockfile/--immutable). A frozen lockfile of reviewed versions is the contract; the cooldown governs only the selection of new ones — and a lockfile that already contains a malicious version is not made safe by freezing it. - Define the emergency path before you need it. Keep per-package exemptions short and limited to your own packages; review urgent security updates individually instead of lifting the gate globally.
- Cover the paths the gate cannot see. Pin GitHub Actions to SHAs, audit workflow and bot-identity changes, and treat editor-extension directories (
.claude/,.vscode/) and agent hook configs as persistence locations — they survive uninstalls. - Assume breach anyway. Phishing,
pull_request_target, Actions cache poisoning, a compromised scanner action, a.pthfile, an editor extension — each wave arrives somewhere the last one did not. The cooldown buys detection time; 24/7 detection and response is what spends it.
Verification note: the recommendation (three-day minimum release age, committed config plus lockfile plus emergency path), the Shai-Hulud timeline (September 2025 debut, November 2025 796-package wave, April/May 2026 campaigns, Megalodon, 4 August 2026 latest), the execution mechanics (npm preinstall/postinstall behaviour including failed-install and npm-ci specifics, PyPI .pth startup execution, Lightning __init__ backdoor with _runtime/start.py and router_runtime.js, eighteen-minute Snyk flag, chalk/debug phishing with live-TOTP relay and two-hour window), the durabletask figures (three 19 May 2026 versions, fourteen import-time lines, Linux-only, 28 KB /tmp payload, half-hour window, StepSecurity’s 99-path exfiltration list including the six named AI tool configs), the persistence details (.claude//.vscode/ artifacts surviving uninstall, gh-token-monitor sixty-second poll with rm -rf ~/ retaliation, rotate-first priority), the agent argument (unprompted dependency adds, soft instructions versus hard configuration), the industry precedents (pnpm v11 one-day default, Dependabot three-day default with security-update exemption, OpenAI minimumReleaseAge post-Axios, uv/npm/Poetry opt-in), the delayed-patch caveat and the four named exemption settings, the lockfile-as-contract reasoning with frozen-install flags and transitive coverage (axios/plain-crypto-js), the Megalodon and tj-actions limits (5,561 repositories, forged bot identities), the seven-manager configuration table with units, the three skill names and their stated scope, the sample audit output, the .claude/ joke and closing thesis (both quoted), and the AI-use editor’s note were read first-hand from the Eye Security research blog HTML (van den Berg and Huiting, 15 October 2026) fetched on 16 October 2026. Prior-incident characterisations (Lightning, durabletask, Axios, Megalodon) are Eye’s as reported, consistent with our own linked coverage. We installed nothing and changed no resolver settings.
Sources:
- Eye Security Research — Cooldown or compromise: the three-day rule for npm and PyPI (15 October 2026; primary research — rationale, per-manager configuration, limits, skills)
- al-ice.ai — TeamPCP Microsoft durabletask PyPI Supply Chain (20 May 2026; the fourteen-line, half-hour-window incident Eye uses as exhibit A)
- al-ice.ai — Tensorlake npm 0.5.144 Shai-Hulud Worm With Valid Provenance (8 October 2026; the latest wave reached straight through signed provenance)
- al-ice.ai — Mandiant: Hijacked Coding Assistant Spreads Shai-Hulud Across 100 Internal Repos (6 October 2026; the agent as installer — Eye’s urgency argument in the wild)
- al-ice.ai — Megalodon Mass GitHub CI/CD Supply Chain (28 May 2026; what a registry-age gate cannot touch)