On 23 September 2026 two packages from the MemTensor project shipped a credential stealer:
- the npm plugin
@memtensor/memos-cloud-openclaw-plugin; - the PyPI package
MemoryOS.
Nobody typosquatted a name, and nobody guessed a password. The attacker used the project's own release pipelines, and the project's maintainers spent the morning racing to republish clean versions over the bad ones.
This is what the day looked like from public data.
The packages
| Registry | Package | Bad versions | Clean versions around them |
|---|---|---|---|
| npm | @memtensor/memos-cloud-openclaw-plugin | 0.1.21, 0.1.23, 0.1.25 | 0.1.20 before; 0.1.22 and 0.1.24 in between |
| PyPI | MemoryOS | 2.0.34 | 2.0.33 before |
The two clean npm releases in the middle are the maintainers pushing back.
- They carry the same code as 0.1.20.
- The npm dist-tags are named
clean-inverse-0-1-23andclean-inverse-0-1-25. - Each clean release went out minutes before or after a bad one.
The timeline
| Time (UTC, 23 Sep) | What happened | Where it is visible |
|---|---|---|
| 00:48 to 02:03 | An account in the MemTensor GitHub organisation creates, pushes and deletes the same branch five times. Every push adds a Linux binary under .sckit/ with the commit message "chore: stage credential-isolation carrier". None of the commits is signed | GitHub events API |
| 02:23:04 | npm 0.1.21 published (bad) | npm time map |
| 03:45:44 | npm 0.1.22 published (clean) | npm |
| 03:49:20 | npm 0.1.23 published (bad), 3 min 36 s after the clean one | npm |
| 04:17:19 | First public report: an issue in the plugin's repository | GitHub |
| 04:33:30 | npm 0.1.24 published (clean) | npm |
| 04:36:58 | npm 0.1.25 published (bad) | npm |
| 05:24:10 to 05:24:20 | In the MemOS repository, a commit that is not on main is pushed, the v2.0.34 tag is deleted and recreated on it, and a release fires | GitHub events API |
| 05:25:22 | PyPI MemoryOS 2.0.34 published (bad) | PyPI journal |
| 07:41 | First vendor write-up (Aikido) | Aikido feed |
| 09:16 | Socket write-up | Socket feed |
| 11:05 | First PyPI malware report reaches OSV | OSV |
| 11:24:20 | PyPI quarantines the MemoryOS project | PyPI journal |
| 11:53 | GitHub advisory for the npm package. At first it listed only 0.1.21; 0.1.23 and 0.1.25 were added later | GitHub advisory database |
| 12:36 | OSV imports it | OSV |
| 17:11 | PyPI quarantines the release and lifts the project quarantine | PyPI journal |
| 19:52 | PyPI advisory (PYSEC) | OSV |
| 20:06:50 | PyPI removes 2.0.34 | PyPI journal |
From first bad publish to first public report: 1 h 54 min. From the PyPI publish to its removal: 14 h 41 min.
How the tokens left the pipelines
Both release workflows published with long-lived registry tokens stored as repository secrets. Neither used trusted publishing.
- npm. The release workflow runs on
pull_requestandworkflow_dispatchand publishes with a classicNPM_TOKEN. SafeDep reports that a changed release-confirmation script wroteBASH_ENVinto the job environment, so a bridge script ran before the publish step and handed the token over. The bridge then failed the job on purpose, so the workflow itself never published; the attacker published with the stolen token from outside, which is why 0.1.21 to 0.1.25 have no tag, no release and no Actions run. - PyPI. The workflow runs on
release: publishedand uses the PyPA publish action with a classicPYPI_API_TOKEN. The attacker's commits changedpyproject.tomland a build script to injectBASH_ENVpointing at_pypi_bridge.sh, which sent the upload password to an outside host.
No git tag or GitHub release exists for npm 0.1.21 to 0.1.25; the last tag is v0.1.20. For PyPI the tag existed, but it had been moved onto a commit that was never on main.
What the payload does
There is no install hook. The code runs when the package is used.
- npm.
index.jsloadslib/sckit.js, which starts a bundled binary (.sckit/<os>-<arch>/sckit) detached and silent, when the gateway starts and on every memory recall. On recall it also passes along the user's prompt text. - PyPI.
import memosreaches the same launcher through the logging setup.
Both packages carry six Go binaries, one per platform, about 7 MB each. Their embedded configuration names the campaign cloud-openclaw-semi-nuclear (npm) and memos-semi-nuclear (PyPI) and sets an expiry: 23 October 2026 00:23 UTC for npm, 02:59 UTC the same day for PyPI. Each configuration points at three fronts under skyleen[.]fr, six in all:
- npm:
8a8acaf167b3,0b48fafd6fbe,266297c6df27; - PyPI:
c747d139e7e9,73376a079d87,d4f77a3a8cb0.
A seventh subdomain, the CI endpoint 10729e014d0e.skyleen[.]fr, received the PyPI token.
Vendors who took the binary apart report that it:
- collects npm, PyPI, git, SSH, Vault and cloud credentials, plus any environment variable that looks like a secret;
- talks to its server over an encrypted protocol;
- carries self-delete routines (SafeDep found them in the binary; no vendor reports seeing it run);
- carries templates for publishing new versions with the stolen tokens.
Indicators (all public):
- domain
skyleen[.]frand its subdomains:8a8acaf167b3.skyleen[.]fr,0b48fafd6fbe.skyleen[.]fr,266297c6df27.skyleen[.]fr(npm);c747d139e7e9.skyleen[.]fr,73376a079d87.skyleen[.]fr,d4f77a3a8cb0.skyleen[.]fr(PyPI);10729e014d0e.skyleen[.]fr(CI endpoint that received the PyPI token);
- IP
139.84.223.178(vendor-reported); - the build path
supplychain.local/campaign/cmd/implant; - the Linux x86-64 binary sha256
381ac6dc1715d9298fe81b2a53a11f7b7d78e361ee3a6619ad54f8c4b062cc18(npm) andc1b0998347b489582bae7b7f4930f9831d9ef4b6bc150cfd488ee1a43272dd36(PyPI).
Who published
The registries kept less than you might expect:
- npm erased the publisher of the three bad versions, together with their scripts, their
distblock and their git hash. What survives is the publish time in thetimemap. The clean 0.1.22 and 0.1.24 still show the maintainer account that published them. - PyPI records no uploader for any release, so there is no publisher to read at all. The journal keeps the release, the quarantine and the removal, with times.
- On GitHub, the branch pushes came from an account inside the MemTensor organisation. Whether that account's own credentials or a token it held were used is not public.
What was gone, and when
| Thing | Status on 24 September |
|---|---|
| npm bad versions on registry.npmjs.org | gone (404); publish times still in the time map |
| npm bad versions on the largest China mirror | gone, same as npm |
| npm 0.1.23 and 0.1.25 tarballs | still served by another public npm mirror, with the hashes matching the OSV record |
| npm 0.1.21 | no full tarball found anywhere; single files still served from the jsDelivr edge cache, with the full file list |
| PyPI 2.0.34 sdist | still downloadable from files.pythonhosted.org by its direct path, after the release was removed from the project page |
| PyPI 2.0.34 wheel | not found |
| GitHub Actions runs for 22 to 24 September | not visible (deleted or hidden) |
Takedown removes a version from the place most people look, not from every place a build can fetch it. A pinned lockfile that names a mirror, or a cache that already holds the file, can still install a version the registry has removed.
What the publisher told us before the payload did
The first vendor analysis came at 07:41. The publisher's behaviour had been visible in public data since 00:48:
- a branch created, pushed and deleted five times in 75 minutes;
- unsigned commits;
- three npm versions published with no tag, no release and no Actions run;
- a PyPI tag moved onto a commit that was never on
main; - a PyPI source release about 25 times the size of the one before it (18.9 MB against 746 KB);
- clean and bad versions minutes apart, under dist-tags named
clean-inverse-0-1-23andclean-inverse-0-1-25.
None of this is in the package bytes. A scanner that judges the package sees it only once the payload is known. Watching the publisher and the pipeline sees it at publish time.
That is what Hour Zero watches: the publisher behind every dependency, not just the package.
What to do if you use either package
- npm. Any version from 0.1.21 to 0.1.25 other than 0.1.22 and 0.1.24 is bad. Move to 0.1.24 or 0.1.20.
- PyPI. Remove 2.0.34 and pin 2.0.33.
- If a bad version ran: rotate every credential the process could read, meaning npm and PyPI tokens, git credentials, SSH keys, Vault tokens and cloud credentials. Check your npm and PyPI accounts for versions you did not publish.
- If you publish packages: move releases to trusted publishing, so a leaked repository secret is not a publish token.
Sources
- The Hacker News: https://thehackernews.com/2026/09/compromised-memtensor-packages-deliver.html
- Aikido: https://www.aikido.dev/blog/supplychain-local-memtensor-npm-pypi
- SafeDep: https://safedep.io/memtensor-sckit-worm-npm-pypi/ and https://safedep.io/sckit-go-implant-framework
- Socket: https://socket.dev/blog/memtensor-compromise
- StepSecurity: https://www.stepsecurity.io/blog/sckit-supply-chain-worm-hits-memtensor-npm-pypi-scopes
- Semgrep: https://semgrep.dev/blog/2026/the-ai-ecosystem-has-worms-now-inside-the-memtensor-compromise/
- GHSA-mhjf-v53x-7p87; OSV MAL-2026-16476, MAL-2026-16475; PYSEC-2026-3987
- Plugin issue: https://github.com/MemTensor/MemOS-Cloud-OpenClaw-Plugin/issues/173