The chronology, from the vendor advisory and the forensics that caught it¶
OVHcloud's advisory and StepSecurity's detonation of the payload agree on every load-bearing fact, and the operational table of them fits in one screen:
| What | When (UTC) |
|---|---|
| Initial access: malicious VS Code extension color-picker-universal 2.8.91 on a maintainer's personal MacBook | Oct 20, 2025 |
| Dormancy: ordinary pull requests to ovh/ovhcloud-docs, May–September 2026 | Mar–Oct 2026 |
| Dry run: four personal repos force-pushed with hidden preinstall.js loaders (ForceMemo pattern, same Solana dead drop) | Mar 5–6, 2026 |
| Branch deletes named dev/test-changest, test/trigger-probe — CI-trigger probing, in hindsight | Oct 15, 2026 |
| The swap: binaries replaced on v0.11.0–v0.15.0, newest first, ~25 minutes; checksums.txt replaced last | Oct 19, 2026, 12:47–14:21 |
| Detection: customer's CI, which pins the SHA-256 of the arm64 tarball, fails; issue #300 preserves evidence | 94 min in |
| OVHcloud deletes swapped assets; clean, immutable, attested v0.16.0 published; advisory posted | Oct 19, from 17:16 |
The mechanism — and this is the part that transfers to any community's release pipeline, including the self-hosted forges and registries this site's audience runs — is that the release artifact channel is mutable by anyone with write access to the repository, at stable URLs, forever afterward, unless immutability is enabled. No tag moved. No CI workflow ran. The source repository was never touched. The attacker uploaded a locally built binary straight onto five months-old releases and replaced checksums.txt after the binaries, so the published hashes matched the poisoned files. A check that compares the downloaded artifact's hash against the release's own checksums.txt — the check almost everyone runs — certifies a lie against an accomplice. The only check that worked compares against a hash recorded somewhere the attacker could not rewrite: a customer CI that pins the artifact digest itself.
One more forensic detail worth keeping: the swapped binary was not built by the release pipeline. go version -m on it shows a pseudo-version (v0.15.1-0.20261013083505-...+dirty, tip-of-main from October 13) and is missing the GoReleaser ldflag the official artifacts carry. The one-command tell is build metadata; StepSecurity documents it as the reliable marker where the vcs-dirty bit alone is not.
What the payload wanted: the honest blast radius¶
Per the advisory, the stealer targets — reading like an inventory of a modern operator's home directory — AI-agent credential stores (Claude, Hermes, Continue, Cody, OpenCode), the AWS/GCP/Azure/doctl CLIs, Docker and Kubernetes configs, Terraform state and tokens, NPM, SSH keys, .netrc, .gitconfig, staging its collection in /tmp/envchecker-* directories it never cleans up. Roughly 250 downloads of the poisoned artifacts went out in the 94-minute window. Homebrew, go install, source builds and the Docker image were not on the path; the official-looking install.sh was, and it fetches "latest" with no checksum verification at any point — the project's own convenience installer is the failure mode, running releases/latest as an authority.
Recovery, if the window caught an operator: treat the host as compromised, assume exfiltration succeeded, rotate every secret the machine could read from a different machine — reinstalling or updating the CLI revokes nothing — remove /tmp/envchecker-*, and reinstall from the clean v0.16.0, verifying with gh release verify-asset v0.16.0 ovhcloud-cli_Linux_x86_64.tar.gz, which checks the attestation rather than the release's self-supplied checksums.
The coda from the same week¶
Cursor's marketplace shipped trojanized builds 2.1.312 and 2.1.313 of the official Claude Code VS Code extension on October 29 — live for most of one day, up to 5,000 users affected per Reuters, cleaned in 2.1.314. The backdoor carries an SSH persistence layer — authorized_keys entry keyed svc-telemetry, persistence via com.apple.sysmond.plist on macOS, an @reboot cron line on Linux, a "SysMon" scheduled task on Windows — and it survives extension removal; the cleanup requires the specific steps (kill the tunnel processes, remove the plist/cron/task, remove ~/.config/.smt and .smtk, purge the authorized_keys line, delete /tmp/.stk_*, rotate credentials if SSH was reachable).
There is a detail in the forensics that keeps its own line: the backdoor did not work as built. It gates SSH activation on the output of systemsetup -getremotelogin, which under a non-admin macOS account errors and exits 0 instead of answering "Off" — so the activation branch, the one that turns on the SSH daemon, is usually skipped. The payload still dropped its SSH keys, persisted, and reported the machine. Two artifact-channel compromises in ten days, and in the second one, only the attacker's own shell logic error stood between a user's IDE running with full user rights and a live SSH backdoor.
The operator checklist this one earns¶
The lesson has been stated on this site in the webshell-before-the-CVE form and the PHP runtime form; this one is the artifact-channel form, and it is the cheapest to close. Neither failure mode is a CVE in the software you run. Both live in the distribution of software you run, which is a surface a community controls outright once it decides to:
- Release immutability — on every publishing pipeline you operate. On GitHub, releases are rewritable in place unless configured otherwise; the OVHcloud incident exists because a months-old release could be silently rewritten at its stable URL. On a self-hosted Gitea or Forgejo, release assets are edit-with-write-access by default, and the October 6 tags/delete advisory showed even the release records behind them could be destroyed through a bare-tag route. Whatever you publish, publish where the artifact cannot change underneath its tag.
- Record your own hashes, outside the namespace you do not fully control. The only detection that worked here was a digest recorded out-of-band, compared at fetch time. This extends past CLI tools to everything fetched by URL: base images, release tarballs, OCI layers. A checksum pinned in your CI config is honest evidence; a checksum served from the same channel as the file is a rubber stamp.
- Treat install scripts that fetch "latest" without verification as the risk they are — including when the script is official. The
install.shon this box's path never opened checksums.txt. That pattern — "curl latest | trusted" — is the artifact-channel version of trusting a web list's first page: plausible, real, and one silent rewrite away from wrong. - Rehearse the rotation path: the compromise that matters most is the one whose secrets live where the payload reads. The advisory's target list is a mirror to hold up to your own configs — agent credential stores and Kubernetes configs first.
Nothing in this story required a proprietary vendor's software to fail. It required a channel where "the same URL still means the same bytes" was an assumption instead of a property. A community's own forge, registry and mirrors are the places to go make it a property.