Skip to content
All posts

The MemTensor packages, hour by hour

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

RegistryPackageBad versionsClean versions around them
npm@memtensor/memos-cloud-openclaw-plugin0.1.21, 0.1.23, 0.1.250.1.20 before; 0.1.22 and 0.1.24 in between
PyPIMemoryOS2.0.342.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-23 and clean-inverse-0-1-25.
  • Each clean release went out minutes before or after a bad one.

The timeline

Time (UTC, 23 Sep)What happenedWhere it is visible
00:48 to 02:03An 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 signedGitHub events API
02:23:04npm 0.1.21 published (bad)npm time map
03:45:44npm 0.1.22 published (clean)npm
03:49:20npm 0.1.23 published (bad), 3 min 36 s after the clean onenpm
04:17:19First public report: an issue in the plugin's repositoryGitHub
04:33:30npm 0.1.24 published (clean)npm
04:36:58npm 0.1.25 published (bad)npm
05:24:10 to 05:24:20In 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 firesGitHub events API
05:25:22PyPI MemoryOS 2.0.34 published (bad)PyPI journal
07:41First vendor write-up (Aikido)Aikido feed
09:16Socket write-upSocket feed
11:05First PyPI malware report reaches OSVOSV
11:24:20PyPI quarantines the MemoryOS projectPyPI journal
11:53GitHub advisory for the npm package. At first it listed only 0.1.21; 0.1.23 and 0.1.25 were added laterGitHub advisory database
12:36OSV imports itOSV
17:11PyPI quarantines the release and lifts the project quarantinePyPI journal
19:52PyPI advisory (PYSEC)OSV
20:06:50PyPI removes 2.0.34PyPI 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_request and workflow_dispatch and publishes with a classic NPM_TOKEN. SafeDep reports that a changed release-confirmation script wrote BASH_ENV into 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: published and uses the PyPA publish action with a classic PYPI_API_TOKEN. The attacker's commits changed pyproject.toml and a build script to inject BASH_ENV pointing 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.js loads lib/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 memos reaches 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[.]fr and 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) and c1b0998347b489582bae7b7f4930f9831d9ef4b6bc150cfd488ee1a43272dd36 (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 dist block and their git hash. What survives is the publish time in the time map. 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

ThingStatus on 24 September
npm bad versions on registry.npmjs.orggone (404); publish times still in the time map
npm bad versions on the largest China mirrorgone, same as npm
npm 0.1.23 and 0.1.25 tarballsstill served by another public npm mirror, with the hashes matching the OSV record
npm 0.1.21no full tarball found anywhere; single files still served from the jsDelivr edge cache, with the full file list
PyPI 2.0.34 sdiststill downloadable from files.pythonhosted.org by its direct path, after the release was removed from the project page
PyPI 2.0.34 wheelnot found
GitHub Actions runs for 22 to 24 Septembernot 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-23 and clean-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

Back to the blog