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 share a common pattern: malicious code gets published to a package registry, and within hours it’s already been downloaded thousands of times before anyone notices. By the time security researchers flag the package or the registry removes it, the damage is done.
This is the golden hour of supply chain attacks: the window where attackers race to compromise systems before their malicious package gets detected and removed. They exploit the immediate-adoption culture of modern development. When a popular package releases a new version, CI/CD pipelines worldwide pull it automatically within minutes, giving attackers just enough time to compromise thousands of build systems.
Then March 2026 happened. Three major compromises in under two weeks:
April 2026 added more to the pile:
May 2026 then added the clearest illustration so far of why upstream pipeline security and downstream cooldown policies are two sides of the same problem:
The downstream consequences of the TanStack push started surfacing within days. The most public confirmation came from a high-profile observability vendor:
Eight days later the same campaign moved to PyPI, this time with an official Microsoft package as the carrier:
Stepping back, these incidents highlight a simple countermeasure that significantly reduces exposure to this attack mode: dependency cooldowns paired with immutable pinning where possible.
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.
This approach works because of simple economics. Attackers publishing malicious packages face a race against time. Registry security teams, automated malware scanners, and the security community are constantly scanning for suspicious packages. Most malicious packages get detected and removed within days, often hours. A 7-day cooldown means you never touch packages during their most dangerous period.
The math is compelling: if malicious packages are typically removed within 24–72 hours, even a 7-day cooldown gives you a comfortable safety margin. Organizations with cooldown policies during the Nx incident were simply never exposed since the malicious versions had been removed days before their pipelines would have considered them.
It’s important to be precise about what cooldowns solve: Cooldowns address version freshness risk, the risk of blindly adopting new, unvetted releases. They do not mitigate known vulnerability risk. Once a vulnerability is identified and a fix is published, the risk calculus flips: delay becomes the dangerous option.
From a business perspective, the Nx and Shai-Hulud incidents exposed thousands of build systems to credential theft. Even without assigning specific costs per compromised environment, incidents of this scale translate into massive organizational impact across response effort, recovery time, and long-term risk exposure. A cooldown policy costs nothing and would have prevented this entire class of attack.
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.
Teams should still periodically validate this behavior in their own repositories. Cooldown logic is applied at update runtime, and overly broad configuration or exclusions can silently suppress updates if not tested.
Renovate offers similar functionality through its minimumReleaseAge setting (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 sufficiently. A notable behavior change 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. The Renovate minimum release age documentation covers the configuration options.
In Renovate setups with broad package rules, security updates can still appear “pending” unless explicitly excluded from cooldown logic. For this reason, security-specific rules are strongly recommended.
pnpm added the minimum-release-age setting in version 10.16, which filters packages by publish date and automatically remaps dist-tags to versions that meet the age requirement. This preserves semantic version compatibility while enforcing your security delay.
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 when updates are scheduled weekly or bi-weekly, the refresh is a conscious, explicit step: you update the lock file, review the diff, and only then accept newer versions. This creates a de-facto cooldown window, ensuring that dependencies must “age” until the next planned refresh instead of being adopted immediately after release. The key is treating dependency updates as a deliberate, reviewable activity rather than something that happens automatically in the background.
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
Here’s a point that’s easy to miss: cooldowns must effectively apply to your entire dependency graph, not just direct dependencies. A malicious package introduced as a transitive dependency can still reach production even if your direct imports are carefully curated.
Modern dependency update tools can account for this, when they are used with lockfiles and conservative update policies. Tools like Dependabot and Renovate operate on the resolved dependency graph, meaning updates (including transitives) are proposed via lockfile changes rather than silently flowing in. As long as lockfiles are committed and updates are gated, transitive dependencies won’t change unless you explicitly accept an update.
A dangerous anti-pattern is allowing floating transitive dependencies in production while only cooling down direct dependencies. This recreates the golden-hour problem one level down the graph, exactly where attackers increasingly aim.
If you rely on manual version pinning or ecosystems without strong lockfile enforcement, this safety net disappears. In those cases, you must regularly regenerate and review the full dependency graph (for example via mvn dependency:tree, pip-compile, or equivalent tooling) to detect 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.
For emergency overrides, establish a clear process. The security team should approve bypasses with documented justification. Record all cooldown bypasses in your security log for audit purposes.
Such fast-tracked packages deserve additional scrutiny. Where feasible, perform manual or automated review of the delta: look for obfuscation, dynamic code execution, unexpected network access, or new persistence mechanisms. Once the normal cooldown period expires, re-verify that the package remains trustworthy.
What cooldowns don’t protect against
Let's be clear about the strengths and limitations.
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
In other words: 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 in a defense-in-depth strategy. Combine them with SBOM generation, vulnerability scanning, code signing verification, and regular 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 a bit ugly and that’s intentional. It forces an explicit, reviewable decision whenever something is updated. Modern dependency management tools can handle this by pinning to full commit SHAs while still enforcing cooldown periods and proposing updates when new versions are released, keeping the workflow manageable.
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. Preferring npm ci requires an existing lockfile, refuses to reconcile mismatches by rewriting it, and performs a frozen install. In practice, that prevents many silent drifts to newly published malicious versions (but only if the lockfile was already 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.
Short-lived compromises aren’t the only threat model, though. Researchers linked the GlassWorm campaign to dozens of malicious or dormant Open VSX extensions beginning in late 2025, including sleeper packages that appeared benign before they later included malicious payloads after establishing trust. The Prettier-VSCode-Plus attack (November 2025) delivered multi-stage malware through a convincing Prettier clone. A cooldown 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.autoUpdatetofalse. JetBrains IDEs provide similar controls under Plugins. This is one of the highest-impact hardening changes you can make, but it also requires a process to review and approve updates manually. Otherwise, you may miss important security fixes that should be applied promptly. - Disable automatic extension update checks in high-trust environments: Set
extensions.autoCheckUpdatestofalseto prevent VS Code from automatically advertising newly released extension versions before they have been reviewed and approved, since users may otherwise be tempted to simply click “Update” once the prompt appears. - 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 deserve investigation.
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 — for example your artifact store, registry proxy, and required cloud identity endpoints — generic exfiltration becomes meaningfully harder, and stolen OIDC material is less useful unless the attacker can also 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, implement a 7-day cooldown on your automated CI/CD dependency updates and IDE extension updates this week. The configuration is minimal, the protection is immediate, and the risk reduction is real.
For teams worried about being “slowed down”: you’re likely already waiting days or weeks between dependency updates in practice. Cooldowns simply formalize this delay and make sure it applies consistently, including on that one rushed Friday afternoon deploy.
Attackers are counting on you to adopt their malicious packages immediately. Make them wait.