Dependency cooldowns: a simple supply chain fix

Sometimes the best defense is simply waiting

Update
Cooldowns have gone from a per-team recommendation to an ecosystem default: pnpm, npm, Homebrew, and VS Code 1.123 all shipped cooldown mechanisms within weeks of each other. They had reason to. A single campaign (TeamPCP, later tracked as the Miasma worm) ran from March through June 2026 — Trivy, LiteLLM, and axios in March; xinference and Checkmarx in April; the TanStack Mini Shai-Hulud cascade, Grafana, and Microsoft’s own durabletask PyPI SDK in May; the Nx Console VS Code extension (18-minute backdoor, verified publisher) and 73 disabled Microsoft repositories by early June. I update this post as the campaign and the defenses evolve; the latest pass adds the Homebrew and VS Code cooldown features alongside the existing sections on IDE extensions and CI/CD identity hygiene.
Christian Schneider · 27 Jan 2026 · 26 min read

The golden hour problem

TL;DR
Most supply chain attacks, including short-lived campaigns like the Nx incident and the recent wormable Shai-Hulud incarnations, exploit a narrow window between malicious package publication and detection. In DevSecOps consulting engagements, cooldown policies have done the job at zero cost: a 7-day delay puts your pipeline outside the attacker’s window and closes most short-lived blast radii on its own.

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.

Consider the Nx supply chain attack from August 2025: Malicious packages were published to npm at 22:32 UTC on August 26. NPM was alerted at 02:44 UTC and removed all affected versions within an hour. Total exposure window: roughly 4–5 hours. Yet in that brief period, thousands of developers had their secrets exfiltrated, including SSH keys, GitHub tokens, and API credentials. The malware even tried to use local AI CLI tools for reconnaissance, a disturbing first in supply chain attacks.

Then March 2026 happened. Three major compromises in under two weeks:

The axios compromise hit one of npm’s most widely used packages. The attacker hijacked a maintainer account, injected a hidden dependency (plain-crypto-js) that delivered cross-platform malware, and hit both release branches within 39 minutes. The malicious versions were pulled within hours. At that download volume, two hours is enough. A cooldown policy likely would have made this a non-event.
LiteLLM followed the same pattern: malicious PyPI releases with a three-stage credential harvester, removed within hours. Again, cooldowns would have prevented exposure.
The Trivy incident (suspected root cause of the LiteLLM compromise) was a different animal. TeamPCP force-pushed many version tags in the aquasecurity/trivy-action GitHub Actions repository and replaced tags in setup-trivy, redirecting existing trusted references to malicious commits. In that tag-hijack scenario, no new version needs to be published, so cooldowns alone can’t help. What can: pinning to immutable commit SHAs in addition to cooldowns.

April 2026 added more to the pile:

The xinference PyPI compromise (April 22, 2026) hit a Python package with 680K downloads and a 9.3K-star repo. Three poisoned releases (2.6.0–2.6.2) landed on PyPI with the malicious payload sitting in xinference/__init__.py, which means it ran on every import xinference: CLI launches, service startup, downstream libraries that just touched the package. Each of those code paths exfiltrated cloud credentials, SSH keys, and .env contents to the attacker’s C2. Maintainers yanked the versions within hours of the first user report. The payload carried a # hacked by teampcp marker, though TeamPCP publicly denied involvement and pointed at a copycat. Either way, the lesson holds: same pattern as axios and LiteLLM, same defense. A 7-day cooldown keeps your pipeline from ever seeing these releases.
The same day, a Checkmarx supply chain compromise hit several distribution channels at once: poisoned tags on the public KICS Docker image, a malicious tag in the GitHub Action repository, and tampered Checkmarx VS Code extensions on both the Microsoft and Open VSX marketplaces. Exposure windows were short, roughly half an hour for the Docker side and about 80 minutes for the Action. This was the second Checkmarx incident in about a month, after a March 23 compromise of their GitHub Actions and OpenVSX plugins. Like Trivy, another security vendor’s distribution channel became the attack vector, and the same defenses apply: pin GitHub Actions to commit SHAs, pin container images to digests (@sha256:<digest>) rather than mutable tags like latest. And since IDEs auto-update extensions in the background by default, reviewing or disabling those auto-update settings is worth doing, at least for security-critical tooling. Two hits in 30 days against the same vendor also says something on its own: when distribution channels are the soft target, repeat compromises aren’t surprising.

May 2026 then linked upstream pipeline security and downstream cooldown policies in the clearest way yet:

The TanStack compromise (May 11, 2026) was a Mini Shai-Hulud variant that pushed 84 malicious versions across 42 @tanstack/* packages in roughly six minutes. The entry point was a pull_request_target workflow in TanStack/router that ran fork-controlled code with elevated trust, combined with actions/cache poisoning across the fork-to-base trust boundary and runtime extraction of an OIDC token from the runner process. The attacker then used the project’s trusted-publisher binding to publish straight to npm. @tanstack/react-router alone pulls roughly 12 million weekly downloads, so the blast radius was large. Maintainers and npm yanked the malicious versions within hours after the first community reports. Wiz attributes the campaign to TeamPCP, the same crew behind Trivy, Checkmarx, and several other 2025–2026 incidents.

Two things are worth pulling out. First, this is the exact upstream-pipeline failure mode I covered in my previous Ship fast, but guard faster post: pull_request_target with a fork checkout is the classic “Pwn Request” anti-pattern, and permissions: contents: read does not stop actions/cache from writing across the fork-to-base trust boundary. Second, even with a clean compromise at the source, downstream consumers who held off on accepting fresh versions for a few days were never exposed. The malicious releases were already gone before their pipelines would have considered them. Cooldowns do not fix the upstream maintainer’s pipeline. They keep your project from being collateral damage when somebody else’s pipeline fails.

The downstream consequences of the TanStack push started surfacing within days. The most public confirmation came from Grafana Labs:

Grafana Labs disclosed on May 16 that attackers tied to the TanStack campaign had reached parts of its GitHub environment. The Grafana team detected suspicious activity on May 11 — the same day the malicious @tanstack/* versions hit npm — and rotated what it described as “a significant number” of GitHub workflow tokens in response. They missed one. Attackers used that single leftover token to keep accessing repositories and then sent Grafana a ransom demand on May 16. The Unit 42 write-up on the broader campaign explains why the rotation set was so hard to get right: the May 11 wave chained pull_request_target with actions/cache poisoning, then extracted an OpenID Connect token from runner memory before publishing malicious packages. So the credentials in scope for Grafana’s response weren’t a long-lived PAT or two. They were a mix of static workflow secrets, short-lived OIDC bindings, and runner-internal artifacts spread across many workflows.

A 7-day cooldown on newly published @tanstack/* versions would likely have kept most downstream consumers outside the initial malicious publication window. For organizations whose pipelines had already resolved the compromised versions before the takedown, the next-best control is a rotation playbook that actually verifies the rotation finished. Grafana caught the cascade on day one and still left one credential behind. That is the part of this incident worth coming back to in the identity-hygiene section below.

Eight days later the same campaign moved to PyPI, this time with an official Microsoft package as the carrier:

The durabletask compromise (May 19, 2026) hit Microsoft’s official Python SDK for Azure Durable Functions, a package with roughly 400K monthly downloads. The attacker pushed three malicious versions directly to PyPI inside a 35-minute window, with no matching tags or workflow runs in the GitHub repository. The release pipeline was bypassed entirely, which points to a stolen PYPI_API_TOKEN or a compromised maintainer account. The project did not use PyPI’s Trusted Publishing (OIDC). The injected dropper sits in __init__.py and only fires on Linux, so the real target is CI runners and cloud workloads rather than developer laptops. It pulls down a 28 KB modular framework that harvests credentials from AWS, Azure, GCP, Kubernetes, Vault, and the local password managers (Bitwarden, 1Password, pass, gopass), then propagates to additional hosts via aws ssm send-command and kubectl exec. Microsoft yanked the affected versions within about two hours of the first upload.

Two things to pull out of this one. First, an official Microsoft package, with hundreds of thousands of monthly downloads and a real maintenance team behind it, still shipped malicious releases for roughly two hours before being pulled. Brand and provenance buy you a faster takedown, not a safer package. The only reliable defense against a two-hour compromise window is not being inside it: a 7-day cooldown on PyPI updates would have kept consumers from ever resolving the compromised versions. Second, this is the textbook argument for PyPI’s Trusted Publishing on the publisher side. A long-lived PYPI_API_TOKEN is one secret an attacker can lift once and use forever, while an OIDC binding only authenticates uploads that actually originate from the configured CI/CD job, on the configured tag and workflow. If you maintain a PyPI package, this incident is the cue to migrate.

Two weeks later the same crew, now tracked under the Miasma worm label, hit two more major vendors in five days:

The Red Hat Cloud Services npm compromise (June 1, 2026) hijacked a GitHub account to push malicious code into at least 32 releases under the @redhat-cloud-services npm scope — frontend libraries behind the Hybrid Cloud Console, roughly 80K weekly downloads combined. The payload shipped as a preinstall script, so it ran on every npm install before any application code, then swept up GitHub Actions secrets along with AWS, GCP, Azure, Kubernetes, Vault, npm, and CircleCI tokens. Red Hat pulled the affected versions after disclosure. Same npm shape as axios and the TanStack cascade, same defense: a 7-day cooldown on @redhat-cloud-services updates keeps your pipeline from ever resolving a poisoned release.
Four days later it reached Microsoft’s own code. On June 5, 2026, GitHub disabled 73 Microsoft repositories across the Azure, Azure-Samples, Microsoft, and MicrosoftDocs organizations after the Miasma worm spread through them. Using stolen contributor credentials, the attacker pushed a malicious commit into Azure/durabletask — a sibling of the durabletask Python SDK poisoned on PyPI two weeks earlier — that planted configuration files which executed a credential stealer whenever the repo was opened in an AI coding tool such as Claude Code, Gemini CLI, Cursor, or VS Code. Each batch of harvested credentials let the worm reach the next repo; GitHub’s automated takedown ran in about 105 seconds. The steal-on-checkout trigger is a new wrinkle, but the GitHub Actions repos caught in the blast (the Azure Functions Action among them) point back to the same control as Trivy and durabletask: pin Actions to immutable commit SHAs, not mutable tags, so a poisoned push into a trusted repo never quietly becomes your next build input.

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.

On May 18, 2026, the Nx Console VS Code extension — over 2.2 million installs, verified publisher (nrwl.angular-console) — was backdoored for 18 minutes. The trojanized version harvested credentials from 1Password vaults, Claude Code configs, npm tokens, GitHub tokens, and AWS keys. That breach spread to GitHub itself and TeamPCP exfiltrated roughly 3,800 of GitHub’s own internal repositories before the compromised version was pulled. The entry point: the Nx developer’s own account had been compromised via the earlier TanStack supply chain attack described above. A pull_request_target misconfiguration in one npm project cascaded into a backdoored popular IDE extension on many developer machines, and then to GitHub itself.

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.autoUpdate to off (since 1.123 the setting is a string enum; older false/true values 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 hard off is a non-starter for your team, delayed at least buys six hours instead of the default two.
  • Disable update checks in high-trust environments: Set extensions.autoCheckUpdates to false. 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-versions periodically 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_target unless 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.



If this resonated...
I help teams lock down their CI/CD pipelines, from dependency policies to build integrity. More info: DevSecOps Pipeline Consulting.