The golden hour problem
Read on if your build pipelines auto-adopt new dependency versions — a zero-cost delay policy eliminates most supply chain attack windows.
Most supply chain attacks follow the same arc. Malicious code lands on a package registry. Within hours it has been downloaded thousands of times. By the time anyone flags it, the damage is done.
This is the golden hour of supply chain attacks: the window in which attackers race to compromise systems before their package gets detected and pulled. They exploit the immediate-adoption habits of most CI/CD pipelines. A popular package publishes a new version, pipelines worldwide pull it within minutes, and the attacker has all the time they need.
Then March 2026 happened. Three major compromises in under two weeks:
April 2026 added more to the pile:
May 2026 then linked upstream pipeline security and downstream cooldown policies in the clearest way yet:
The downstream consequences of the TanStack push started surfacing within days. The most public confirmation came from Grafana Labs:
Eight days later the same campaign moved to PyPI, this time with an official Microsoft package as the carrier:
Two weeks later the same crew, now tracked under the Miasma worm label, hit two more major vendors in five days:
Same shape across all of these. Same two defenses, too: dependency cooldowns for short-window publication attacks, immutable pinning for tag-rewrite attacks.
What are dependency cooldowns?
A dependency cooldown is exactly what it sounds like: a waiting period before your tooling accepts new package versions. Instead of immediately adopting version 1.2.4 when it’s published, you wait 5 to 10 days before considering it for your project.
The approach works because the attacker is racing the clock. Registry security teams, automated scanners, and the broader security community are all looking for suspicious packages all the time. Most get detected and removed within 24–72 hours. The timestamps from every incident above bear this out. A 7-day cooldown keeps your pipeline out of that window.
The math is simple: if malicious packages disappear within 24–72 hours, a 7-day delay puts your pipeline outside the danger window every time. The teams running cooldown policies during the Nx incident were never exposed. The malicious versions had been pulled days before their pipelines would have considered them.
Be precise about what cooldowns solve. They address version freshness risk: the risk of blindly adopting new, unvetted releases. They do not mitigate known vulnerabilities already in your tree. Once a fix lands for a published CVE, the calculus flips and delay becomes the dangerous side.
Every one of these incidents triggered the same downstream cleanup for thousands of organizations: rotate credentials, audit logs, hunt for persistence, write the postmortem. A cooldown policy costs nothing and skips the entire exercise.
Tool support
Several dependency management tools now support cooldowns natively:
Dependabot introduced the cooldown option in mid-2025, allowing you to specify minimum age requirements before version updates are proposed. You can configure different delays based on semantic version changes, with longer waits for major versions and shorter ones for patches. Dependabot’s cooldown applies only to routine version updates, not security updates, so CVE patches should still flow through promptly. See the Dependabot cooldown documentation for configuration details.
Test the actual behavior in your own repos before relying on it. Cooldown logic is applied at update runtime, and overly broad configuration or exclusions can silently suppress updates you actually wanted.
Renovate has the same control under minimumReleaseAge (previously called stabilityDays). Renovate creates branches for pending updates but marks them with a “pending” status check until the cooldown expires. If you have automerge enabled, updates won’t merge until they’ve aged enough. In Renovate 42: packages without a release timestamp are now treated as if they haven’t passed the cooldown period, which is safer than the previous behavior (where missing timestamps fell through). The Renovate minimum release age documentation covers the configuration options.
In Renovate setups with broad package rules, security updates can still sit “pending” unless explicitly excluded from cooldown logic. Add explicit security-specific rules that bypass the cooldown.
pnpm added the minimum-release-age setting in version 10.16. It filters packages by publish date and automatically remaps dist-tags to versions that meet the age requirement. Semver ranges keep working; the delay still gets enforced. Since pnpm 11 the setting is on by default at 1440 minutes, making pnpm the first major package manager to ship a 24-hour cooldown out of the box. npm itself followed with a native min-release-age config in 11.10.0 (February 2026), off by default but a one-liner in .npmrc.
For ecosystems without native cooldown support, lock files provide a manual alternative. Tools like Poetry, uv, or Go modules with go.sum pin exact versions, including transitive dependencies, so newly published releases are never pulled in implicitly. Even on a weekly or bi-weekly cadence, the refresh becomes a deliberate step: update the lock file, review the diff, accept the newer versions. The cooldown emerges from the process. Treat dependency updates as a reviewable activity, not background plumbing.
Package managers are starting to ship cooldowns as defaults
When I first published this post, cooldowns were something you configured for your own project. That’s changing: they’re becoming something package managers apply for their entire user base. Homebrew is the clearest example so far.
In April 2026 the Homebrew maintainers added cooldowns to the ecosystems with the worst recent track record: npm and PyPI. The brew bump change teaches Homebrew’s autobump automation to ignore freshly published npm and PyPI releases and select the newest version published before the cooldown cutoff instead. A companion change covers build time: Node formulae now install npm dependencies with a one-day --min-release-age, and Python builds pass pip an --uploaded-prior-to timestamp with the same one-day window. PyPI resource resolution got the same treatment, and Homebrew extended cooldowns to its RubyGems handling in May and Bundler in early June. Note the scope: this covers formulae and their build-time npm/PyPI resolution. Casks and third-party taps sit outside it.
Where Homebrew puts the delay is the instructive part. The cooldown sits on the maintainer side, applied once when a formula (Homebrew’s package recipe) is bumped, and Homebrew’s supply chain security documentation explains why: a second user-side delay would be a “double cooldown” that postpones zero-day fixes twice, and Homebrew’s model of human-reviewed version bumps, pinned checksums, and bottles built from source already provides the review window that language-ecosystem cooldowns try to recreate. So every brew install inherits the protection without anyone touching a config file. That layering also explains why Homebrew can afford a 24-hour window where I recommend seven days: their cooldown is one control in a stack that includes a human reviewing every version bump. Your CI’s npm install has no such reviewer. There the 7-day recommendation stands.
One practical takeaway hides in the implementation: the primitives Homebrew leans on are plain npm and pip flags, usable in any pipeline, including ones where Renovate or Dependabot aren’t in the picture:
npm install --min-release-age=7 # days; npm >= 11.10.0, or set min-release-age=7 in .npmrc
pip install --uploaded-prior-to=P7D # ISO 8601 duration; pip also accepts a timestamp
(uv users get the same primitive as --exclude-newer.) Two caveats before you roll these out. pip’s flag only works against indexes that expose upload-time metadata, so behind some registry mirrors it silently does nothing. Verify it bites before trusting it. And both flags act at resolution time: against a frozen lockfile install like npm ci there is nothing to resolve, so their real value is the installs that bypass lockfiles entirely. Global tool installs in Dockerfiles, npx one-offs, ad-hoc pip install in container builds: exactly the installs your update bot never sees. npm at least fails closed, erroring out if no version is old enough rather than installing a fresh one.
VS Code went the same direction for extension updates two months later; more on that in the IDE extensions section below.
A common misconfiguration trap
One recurring failure mode I see in audits is teams enabling cooldowns, assuming they are “safe,” and then relaxing their active monitoring of security advisories. Cooldowns reduce exposure to unknown malicious releases. They do nothing for known vulnerabilities already present in your dependency tree.
Without active vulnerability alerting and triage, cooldowns can actually increase dwell time for exploitable CVEs. Cooldowns are a preventive control, not a detective one.
Transitive dependencies: the hidden risk
Cooldowns must apply to your entire dependency graph, not just direct dependencies. A malicious package introduced as a transitive can still reach production even when your direct imports are carefully curated.
Dependabot and Renovate handle this if you use them with lockfiles and conservative update policies. They operate on the resolved dependency graph, so updates (including transitives) show up as lockfile changes rather than flowing in silently. As long as lockfiles are committed and updates are gated, nothing transitive changes without you accepting it.
The anti-pattern: floating transitive dependencies in production with cooldowns only on direct dependencies. That just moves the golden-hour problem one level down the graph, which is exactly where attackers increasingly aim.
If you rely on manual version pinning or ecosystems without strong lockfile enforcement, this safety net disappears. Rebuild and review the full dependency graph on a schedule (for example via mvn dependency:tree, pip-compile, or equivalent tooling) to catch unexpected transitive additions or version shifts.
Handling urgent security patches
Cooldowns work best when paired with an explicit security SLA, for example: critical dependency CVEs must be triaged within 24 hours and patched within 72.
Cooldowns should apply to routine updates, not emergency security patches. Dependabot explicitly excludes security updates from cooldown rules. Renovate allows you to force immediate updates for specific packages through its Dependency Dashboard or security-specific rules.
Bypasses need a paper trail. The security team approves, the reason gets logged. When this comes up in an audit later, the question won’t be “did you patch fast?” but “why did you skip the cooldown?” Have the answer ready.
Fast-tracked packages get extra scrutiny. Review the diff manually or with tooling: obfuscation, dynamic code execution, unexpected network calls, new persistence mechanisms. Once the normal cooldown period expires, recheck the package against whatever fresh advisories have surfaced in the meantime.
What cooldowns don’t protect against
Worth being precise about the limits.
Dependency cooldowns are effective against:
- Compromised maintainer accounts with short-lived malicious releases
- Automated malware injection and wormable release pipelines
They are not effective against:
- Typosquatting attacks using similar package names
- Long-term maintainer compromise
- Zero-day vulnerabilities where fixes must be applied immediately
Cooldowns buy you time, not certainty. Use that time to let scanners run, advisories surface, and the community react. Then decide from a position of information, not urgency.
Cooldowns are one layer. The rest of the usual stack still applies: SBOM generation, vulnerability scanning, signature verification, dependency audits. Yes, Trivy itself was compromised in March 2026. The irony isn’t lost on me. The scanner is still solid — the distribution channel was the problem, and pinning to commit SHAs would have prevented it:
Pinning to immutable references
As the Trivy incident showed, cooldowns don’t help when the attacker rewrites existing version references rather than publishing new ones. Git tags are mutable. Commit SHAs are not. The fix: pin GitHub Actions (and any Git-referenced dependency) to full commit SHAs instead of version tags.
Instead of: uses: some-org/some-action@v1 use full commit SHA: uses: some-org/some-action@28f2510ee396bbf400402947e7a9c4e3f7e3b144
It looks ugly, and that’s intentional. The full SHA forces an explicit, reviewable decision every time something is updated. Dependency management tools can handle this: pin to full commit SHAs, still enforce cooldowns, propose updates as new versions appear. The workflow stays sane.
GitHub’s Immutable Releases feature helps, but it is not a substitute for pinning. In the Trivy incident, some artifacts were protected by immutable releases, while mutable tags in GitHub Actions were still force-pushed to malicious commits. Pin to the full SHA.
For npm, the lockfile integrity hashes (sha512-... in package-lock.json) provide a similar control: they bind installs to specific resolved artifacts. That’s why committing your lockfile and using npm ci matters. npm ci requires an existing lockfile, refuses to reconcile mismatches by rewriting it, and performs a frozen install. In practice that blocks most silent drifts to newly published malicious versions, provided the lockfile was committed before the compromise.
Pin to content-addressed references wherever possible. Version tags, branch names, and semver ranges are all mutable indirections. Commit SHAs and integrity hashes are not.
IDE extensions: the developer-side blind spot
IDE extensions are dependencies. They just don’t show up in your package.json, pom.xml, requirements.txt, or go.mod, so nobody treats them that way. But a VS Code extension with workspace trust reads your files, sees your environment variables, and can reach locally stored credentials. Many have terminal and network permissions. And unlike your CI/CD dependencies, they update silently in the background — no pull request, no lockfile diff, no review step.
Eighteen minutes. That was the entire exposure window. “Verified publisher” meant nothing here — the legitimate publisher’s credentials were the weapon.
If you’ve been following the cooldown argument through this post, the pattern should feel familiar by now. If VS Code hadn’t auto-updated the moment the compromised version landed on the marketplace, nobody would have been exposed. The malicious release was pulled faster than most standups run. The only developers hit were the ones whose IDE silently adopted it in real time. Same golden-hour economics, different distribution channel.
Microsoft drew the same conclusion. VS Code 1.123 (June 3, 2026) ships a built-in cooldown for extensions: with automatic updates enabled, a newly published version is installed only two hours after publication. The extension’s details view shows why the update is pending and when it will land, and a manual Update click still installs immediately. Two hours would have covered the entire 18-minute Nx Console window, so credit where due. Note the scope, though: the delay applies to automatic updates. A fresh install of a just-published version, whether from the marketplace UI or code --install-extension, is not delayed at all.
Can you raise the two hours? Not meaningfully. There is no duration setting (at least not yet). The extensions.autoUpdate setting became a three-value enum (on | delayed | off) in the same release, and per the 1.123 test plan the delayed mode stretches the window to six hours. That is as far as the dial goes, and two hours says something about the threat model Microsoft is optimizing for: loud, fast-burning compromises that the marketplace yanks quickly. It sits well below the 24–72 hours that detection typically takes for less noisy malicious releases. The manual Update button is also a softer gate than it looks: the delay assumes users wait passively, and an attacker who pairs a poisoned release with a “critical fix, update now” nudge gets the click inside the window. For a real cooldown you are back to off plus a manual review step, which is where the recommendations below land anyway. There is an open feature request for a configurable minimum release age in days, enforceable via enterprise policy; if your organization manages VS Code fleets, that one is worth your thumbs-up. Until it lands, fleet enforcement means pushing extensions.autoUpdate through your managed-settings tooling and leaning on extension allowlists, with the caveat that a developer can revert what isn’t centrally locked.
The exemption is the part I would push back on: extensions from publishers Microsoft deems trusted, such as Microsoft itself, GitHub, and OpenAI, skip the delay entirely and keep updating the moment they publish. Set that against the incidents above. This spring’s campaign shipped malicious versions of an official Microsoft package on PyPI and wormed through 73 Microsoft repositories on GitHub. “Verified publisher” was exactly the credential that made Nx Console dangerous. An exemption list built from the ecosystem’s most attractive targets is a strange place to park residual risk, and there is no setting to opt trusted publishers back into the delay; the only way to close the gap is off, which stops auto-updates across the board. One more boundary worth knowing: VS Code forks like Cursor, Windsurf, and VSCodium lag upstream and mostly pull extensions from Open VSX, the marketplace this post already flagged as weaker. Don’t assume they inherit any of this.
Short-lived compromises aren’t the only threat model, though. Researchers linked the GlassWorm campaign to dozens of malicious or dormant Open VSX extensions starting in late 2025, including sleeper packages that looked benign for months and then slipped malicious payloads in once they had earned install counts and trust. The Prettier-VSCode-Plus attack (November 2025) delivered multi-stage malware through a convincing Prettier clone. A cooldown measured in days catches both patterns: the delay gives scanners and the community time to flag problems before the extension ever reaches your machine.
Extension marketplaces also have weaker vetting than package registries. npm, PyPI, and crates.io run automated malware scanning and support Trusted Publishing flows. The VS Code Marketplace’s “verified publisher” badge confirms the publisher’s domain ownership — it says nothing about what the code in the .vsix actually does. Open VSX checks even less. The Checkmarx compromise I covered above hit their VS Code extensions on both marketplaces, and the repeat incident a month later showed that once a distribution channel is identified as a soft target, attackers come back. When a package registry and an extension marketplace both face supply chain pressure, the marketplace cracks first.
What to do about it
- Disable automatic extension updates: In VS Code, set
extensions.autoUpdatetooff(since 1.123 the setting is a string enum; olderfalse/truevalues migrate automatically). JetBrains IDEs have similar controls under Plugins. This is one of the most impactful single hardening changes you can make. The catch: you need a review process for manual updates, or you’ll miss security fixes that should land promptly. In practice a weekly 15-minute pass over pending updates by a rotating owner covers most teams. If a hardoffis a non-starter for your team,delayedat least buys six hours instead of the default two. - Disable update checks in high-trust environments: Set
extensions.autoCheckUpdatestofalse. Otherwise users see “Update available” prompts and tend to click them before anyone has reviewed the release. - Use extension allowlists: VS Code profiles and organizational policies let you restrict which extensions are permitted. If your team doesn’t need an extension, don’t install it.
- Treat extension updates like dependency updates: Review changelogs, check release timing, wait a few days before accepting. The same 7-day rule of thumb works.
- Diff your installed extensions: Run
code --list-extensions --show-versionsperiodically and compare against a known-good baseline. Unexpected version bumps or new additions are the things worth investigating.
When token rotation isn’t enough
The Grafana case above illustrates a failure mode that no cooldown policy can save you from: you are the downstream consumer of somebody else’s compromised pipeline, you detect the cascade on day one, you rotate tokens — and you still miss one. From there, a single leftover credential is enough to keep the attacker inside your perimeter for days.
This pattern shows up again and again in public CI/CD incident write-ups. Teams treat “rotate tokens” as a cleanup task rather than as an engineering problem with verification requirements.
A few practical implications
- Inventory CI/CD identities the way you inventory production service accounts: Every workflow token, every OIDC trust binding, every deploy credential needs an owner, a scope, and an expiry. If you cannot enumerate them on demand, you cannot rotate them on demand either.
- Make “rotate” a playbook with a verification step: After rotation, run a canary job that attempts to use the old credential set against the repositories, registries, and cloud accounts it previously had access to. The job should fail closed. If it succeeds, the rotation isn’t done.
- Reduce blast radius at the workflow boundary: PR-validation workflows should not be able to write caches that release pipelines later consume. Avoid
pull_request_targetunless you fully isolate the fork checkout and lock down permissions for the elevated context. This is the same upstream lesson the TanStack incident hammered home from the maintainer side; here it shows up as a downstream control. - Add egress controls on runners: If runners can only reach explicitly approved destinations (your artifact store, registry proxy, required cloud identity endpoints), generic exfiltration becomes much harder. Stolen OIDC material is also less useful unless the attacker can reach a permitted token-exchange or deployment target.
Cooldowns, pinning, IDE extension hygiene, and identity hygiene are stacked controls for different failure modes. Cooldowns keep you out of compromised upstream windows. Pinning blunts tag-rewrite attacks. A verified rotation playbook is what saves you when somebody else’s pipeline already failed and the cascade reached you anyway.
For more practical tips on locking down your supply chain and secure build pipelines, see my post about how to ship fast, but guard faster: securing DevOps itself.
Getting started today
Rule of thumb: Delay unknown updates by default, fast-track known security fixes deliberately.
If you take nothing else from this post, set a 7-day cooldown on your automated CI/CD dependency updates and IDE extension updates this week. The config is minimal. The protection is immediate.
For teams worried about being “slowed down”: you’re already waiting days or weeks between dependency updates in practice. Cooldowns just formalize that delay and make sure it applies consistently, including on the one rushed Friday afternoon deploy nobody wants to think about.
The ecosystem has started validating the approach: pnpm, npm, Homebrew, and VS Code all shipped cooldown mechanisms within a few months of each other. Treat their hour-scale and day-scale defaults as a floor, not as a substitute for your own 7-day policy.
Attackers are counting on you to adopt their malicious packages immediately. Make them wait.