Updated October 23, 2026: we corrected our detection timings, the block date and the package count against our own records. Earlier versions of this post said our wave correlator grouped the wave and our scorer made block decisions as it happened, and that we caught it days before the first public write-ups. In fact, our first alert came 30 minutes after the first malicious publish, and a public post about the wave followed five minutes later. The correlator grouped the packages on June 9, after the fact, and the first point at which our proxy would block an install was June 10. The campaign covered 38 projects and 68 versions. We alerted on 37 projects and 63 versions, and missed one. We also corrected the api[.]anthropic[.]com/v1/api indicator: it is a path that does not exist on Anthropic's real API host, not an attacker server. We removed a line saying we send a sandbox trace to a model, because our June 5 alerts came from a scanner finding on the .pth file and no sandbox trace was involved.
The Hades payload runs the moment a poisoned package is imported, not just at install time, and it targets CI runners as well as workstations. If a build agent or developer laptop pulled one of the versions listed below, rotate every credential that machine could reach (GitHub, npm, PyPI, cloud, SSH) and audit recent GitHub Actions runs and new public repositories under your accounts.
What happened
A threat actor that researchers track as Miasma published a fresh wave of malicious packages to PyPI in early June 2026. The newest branch of the operation, named Hades after the underworld theme woven through its infrastructure, poisoned 38 PyPI projects across 68 versions. It is part of the broader Mini Shai-Hulud lineage, a family of self-propagating supply-chain worms that steal developer and CI secrets and use them to publish the next generation of infected packages.
What makes Hades worth a close read is the delivery mechanism. Instead of a classic setup.py install hook, the operators shipped a Python path-configuration file, a .pth file, alongside an obfuscated JavaScript payload named _index.js. Any line in a .pth file that begins with import is executed by the Python interpreter every single time the environment starts. That turns a quiet one-line file into reliable code execution that fires on import rather than on install, and it survives in the site-packages directory long after the package looks installed and idle.
The campaign was aimed with intent. Two clusters stand out: bioinformatics tooling used for graph learning, patient phenotyping, and phenopacket workflows, and a set of packages dressed up as Model Context Protocol and AI developer tools. The names were chosen to look at home in the import lists of research labs and AI engineering teams, two communities that move fast and pull niche packages without much friction.
The loader opens with a block of fake system instructions written as a JavaScript comment, a deliberate prompt injection meant to convince automated AI reviewers that the package is clean infrastructure. The real logic sits behind a try{eval(...)} wrapper, a character-code array, and a ROT-style substitution routine, with sixteen functional payloads each encrypted under its own AES-256-GCM key and gzip-compressed. A scanner that reads source strings sees almost nothing, and an LLM reviewer that ingests the raw file can be talked out of its own verdict. Our June 5 alerts did not depend on decoding _index.js: the tarball scanner flagged the .pth startup file itself, because it contains a line that runs code every time Python starts.
How CyberXYZ flagged the wave about 30 minutes in
Our PyPI firehose ingested the malicious releases as they were published, from June 5 to June 8, 2026. The first malicious release, bramin 0.0.2, went up at 23:15 UTC on June 5. Our scanner had seen earlier, clean releases of a few of these projects in the two weeks before the wave, but nothing in them pointed to the attack. The scanner's first finding came at 23:45 UTC, and the first HIGH alert went out in the CyberXYZ app at 23:46 UTC, 30 minutes after that first publish. The remaining versions of the June 5 wave were alerted at 00:02 UTC on June 6. That was hours before the first public advisory (an OSV record published at 06:13 UTC on June 6) and more than a day before the first vendor write-ups (Socket on June 7, StepSecurity on June 8). It was an alert, not a block, and we do not claim to have been first: a public post about the wave appeared on Bluesky at 23:51 UTC, five minutes after our alert.
Across the whole campaign we alerted on 37 of the 38 projects and 63 of the 68 versions. We missed ensmallen 0.8.101: it was over our scanner's 20 MB size limit at the time, so the scanner skipped it.
Three detection layers were involved, but only one of them acted on June 5. The tarball scanner flagged the Python startup hook under the pth_executable_import signal, which is what triggered the June 5 alerts. The other two layers came later, after the public reports. The wave correlator first ran on June 9 and grouped the releases into waves after the fact. On June 10 at 04:16 UTC, the cross-signal scorer promoted 24 versions in 16 packages to the block list. That is the first point at which the CyberXYZ proxy would have blocked an install.
The wave correlator does something per-package scanners cannot. It fingerprints the actual install-hook payload, not the package name or the install command, and groups packages that ship the same malicious code within a tight publishing window. That is the difference between a triage queue full of noise and a single line that reads: a coordinated multi-package campaign is publishing right now. For Hades, that grouping came after the fact. The correlator first ran on June 9, three days after the first publish, and grouped the Hades releases into two waves by their shared startup hooks.
Mass-token theft worms win by volume and speed. A defender who reviews each package in isolation will catch the obvious ones and miss the marginal members. Clustering on the shared payload turns the volume against the attacker: the more packages they push with the same hook, the louder the wave reads.
Timeline of events
- Late May 2026 Earlier Miasma activity seeds the lineage, including npm build-hook worms and IDE and AI-assistant backdoors. Clean releases of a few PyPI projects that the wave later hits pass through the CyberXYZ scanner. None of them is malicious.
- June 5 to 6, 2026 The Hades PyPI wave publishes, starting at 23:15 UTC on June 5. CyberXYZ ingests the malicious wheels, the tarball scanner flags pth_executable_import, and the first in-app HIGH alert goes out at 23:46 UTC. A public post about the wave appears on Bluesky at 23:51 UTC, and OSV publishes the first advisory records at 06:13 UTC on June 6.
- June 7 to 8, 2026 Public analysis of the Hades branch is published. PyPI begins quarantining the affected releases. Total tracked scope reaches several hundred artifacts across npm and PyPI.
- June 9 to 10, 2026 The CyberXYZ wave correlator runs for the first time on June 9 and groups the affected releases into waves after the fact. On June 10 at 04:16 UTC, the scorer promotes 24 versions in 16 packages to the block list, the first point at which the CyberXYZ proxy would block an install. Until then, CyberXYZ alerted on these packages but did not block them.
The attack chain in detail
The execution flow is compact and resilient, built so a single import triggers the whole chain and so it only runs once per host.
- Startup hook fires on import. The poisoned wheel ships a
*-setup.pthfile. Its single executable line checks for a sentinel at/tmp/.bun_ran. If the sentinel is absent, deobfuscated Python runs. - Bun is fetched on demand. The hook scans
sys.pathfor the bundled_index.js, downloads the Bun JavaScript runtime from the official Bun GitHub releases if it is not already present, and writes the sentinel so the host is not infected twice. - The JavaScript stealer runs. The hook executes
bun run _index.js. The bundle decrypts its sixteen payloads at runtime and selects an operating-system-specific module. - Memory and secrets are scraped. On Linux the stealer walks process memory through
/proc, on macOS it calls Mach kernel VM APIs through ctypes without needing root, and on Windows it compiles C# on the fly through PowerShell to read process memory. The high-value target is the CI runner worker process, where it lifts plaintext environment variables and the workflowGITHUB_TOKEN. - Exfiltration hides inside GitHub. Harvested secrets are gzip-compressed, encrypted with a fresh AES-256-GCM key, and the key itself is wrapped with the attacker's RSA public key. The bundle is pushed to a newly created public repository under the victim's own GitHub account. The payload also contains an HTTPS sender aimed at
api[.]anthropic[.]com/v1/api. That is Anthropic's real API host, not an attacker server, and/v1/apiis not a live route on it, so this channel cannot deliver data to the attacker. Socket assesses it as camouflage: traffic to a common AI-vendor host blends into network logs, while GitHub remains the confirmed exfiltration channel.
Hades does not stop at theft. It establishes long-running persistence through user-level service files on Linux and Launch Agents on macOS, drops workspace backdoors into editor and AI-assistant configuration files (for example .cursorrules, .windsurfrules, .github/copilot-instructions.md, and .claude/settings.json) so the payload re-arms inside developer tooling, and injects a GitHub Actions workflow disguised under a benign name to dump repository secrets through the standard artifact upload path. The operators also built a deterrent into the design, wiring destructive behavior to trigger if the stolen token is revoked, which pressures victims to leave the access live.
Two more details show how deliberate this campaign is. First, the loader is not the only delivery path: some samples ship native compiled extensions (.abi3.so files) that execute at import time, so blocking .pth files alone does not close the door. Second, the malware actively hunts for defensive tooling on the host, checking for runtime-security agents and CI hardening tools and their endpoints, and it probes the Docker socket at /var/run/docker.sock for container escape and lateral movement. This is a worm that expects to land on a defended CI runner and tries to work around the defenses it finds.
Indicators of compromise
The following indicators are drawn from published analysis of the Hades branch. File hashes are per-artifact, so treat the names and behaviors as the durable signal and the hashes as confirmation for the specific wheels listed.
| Type | Indicator | Context |
|---|---|---|
| Payload file | _index.js |
Obfuscated Bun-executed JavaScript stealer bundled in the wheel |
| Startup hook | *-setup.pth |
Python path-configuration file that executes on every interpreter start |
| Sentinel file | /tmp/.bun_ran |
Run-once marker written after first execution |
| Propagation file | /tmp/.sshu-setup.js |
Staging artifact used during self-spread |
| SHA-256 | 6506d31707a39949f89534bf9705bcf889f1ecae3dbc6f4ff88d67a8be3d01b2 |
langchain_core-setup.pth |
| SHA-256 | 6d332f814f15f19758d65026bbfd0a8c49671b319ec77b8fa1b27fc48afff7d9 |
langchain_core_mcp-1.4.2-py3-none-any.whl |
| Decoy endpoint | api[.]anthropic[.]com/v1/api |
Non-existent path on Anthropic's real API host, not an attacker server. It returns 404, so it cannot deliver data; assessed as camouflage for network logs |
| Runtime download | github[.]com/oven-sh/bun/releases/download/bun-v1.3.13 |
Bun runtime pulled on demand to execute the payload (v1.3.13 to v1.3.14 observed) |
| Upload marker | Bun/1.3.14 |
User-agent observed on malicious PyPI uploads |
| C2 commit marker | DontRevokeOrItGoesBoom |
Keyword for the encrypted token dead-drop channel |
| C2 commit marker | TheBeautifulSnadsOfTime |
Keyword for the signed JavaScript command channel |
| C2 commit marker | firedalazer |
Keyword for the Python dropper polling channel |
| Exfil repo pattern | stygian-cerberus-[0-9]+, tartarean-charon-[0-9]+ |
Attacker-created public repositories used as encrypted dead-drops |
| Repo description | Hades - The End for the Damned |
Description string set on exfiltration repositories |
| Injected workflow | .github/workflows/codeql.yml (name "Run Copilot") |
Disguised workflow that dumps secrets to the format-results artifact |
| Persistence (Linux) | ~/.config/systemd/user/update-monitor.service |
User-level C2 polling service |
| Persistence (macOS) | ~/Library/LaunchAgents/com.user.update-monitor.plist |
Launch Agent C2 service |
Affected PyPI projects
The 38 affected projects are: bramin, cmd2func, coolbox, dynamo-release, executor-engine, executor-http, funcdesc, magique, magique-ai, mrbios, napari-ufish, nucbox, okite, pantheon-agents, pantheon-toolsets, spateo-release, synago, ufish, uprobe, ensmallen, embiggen, gpsea, pyphetools, ppkt2synergy, phenopacket-store-toolkit, nhmpy, mflux-streamlit, rlask, tlask, rsquests, dreamgen, mem8, orchestr8-platform, ray-mcp-server, langchain-core-mcp, instructor-mcp, openai-mcp, and tiktoken-mcp. Confirm exact compromised versions against the official PyPI advisories before remediation.
Search your site-packages directories for any *-setup.pth file that contains an import statement, and for a bundled _index.js next to an installed package. Look for the sentinel /tmp/.bun_ran and for an unexpected Bun binary in a temp path. On CI, review recent GitHub Actions runs for short-lived workflows named like Run Copilot or files under .github/workflows/codeql*.yml that were added and quickly deleted, and check for an artifact named format-results. Audit your GitHub account for new public repositories you did not create, especially with underworld-themed names or the description string above.
What was compromised
The stealer casts a wide net across developer and CI secrets: GitHub personal access tokens and the workflow GITHUB_TOKEN, npm, PyPI, RubyGems, and JFrog registry credentials, cloud provider keys, Kubernetes service-account material, SSH private keys, Docker configuration, shell histories, and the contents of .env files and AI developer-tool configurations. Because the worm uses stolen registry credentials to publish the next round of poisoned packages, every successful theft is also fuel for the campaign's spread.
Supply-chain risk analysis
Hades is a clean example of why publish-time behavior matters more than package reputation. None of these projects were typosquats of famous libraries in the traditional sense. They were real-looking research and AI tools whose only tell was the malicious code they suddenly started shipping. The relevant techniques map cleanly to the MITRE ATT&CK framework.
| Technique | ID | Use in Hades |
|---|---|---|
| Compromise Software Supply Chain | T1195.001 | Malicious PyPI packages delivered to developers and CI |
| Command and Scripting Interpreter, JavaScript | T1059.007 | Payload executed through the Bun runtime |
| Steal Application Access Token | T1528 | GITHUB_TOKEN and registry credential theft |
| Credentials from Process Memory | T1003.007 | Reading the CI runner worker process memory |
| Unsecured Credentials in Files | T1552.001 | Harvesting SSH keys, .env, and registry config files |
| Application Layer Protocol, Web | T1071.001 | GitHub API and commit markers used as a C2 channel |
| Exfiltration to Code Repository | T1567.001 | Encrypted secrets pushed to attacker-created GitHub repos |
| Event Triggered Execution, Startup | T1546 | Python .pth startup hook firing on every interpreter launch |
Recommended actions
Immediate. Remove any affected package version from your environments and registries. On any host that imported one, rotate GitHub tokens, registry tokens, cloud keys, and SSH keys, and revoke the workflow tokens for affected repositories. Review and remove any unexpected public repositories, GitHub Actions workflows, persistence services, and editor or AI-assistant config backdoors described above. Assume secrets reachable from the host are exposed.
Short term. Pin dependencies and require review for version bumps on the packages your research and AI teams pull. Treat install-time and import-time code execution as a first-class risk in review, not a footnote. Constrain CI runner permissions so a single poisoned import cannot reach every secret in the environment, and prefer short-lived, least-privilege tokens.
Long term. Put a publish-time behavioral gate in front of your ecosystem. Reputation and version age do not catch a trusted project that suddenly ships a startup hook. Correlation across packages does. A worm that pushes the same payload into thirty projects should read as one loud event, not thirty quiet ones.
CyberXYZ watches npm, PyPI, and other registries at publish time, scores each release across behavioral signals, and correlates coordinated waves so a worm surfaces as a single campaign. For the Hades wave, that meant in-app HIGH alerts about 30 minutes after the wave began, hours before the first public advisory. Grouping the waves came later, on June 9, and the first block-list promotions, which let the proxy stop an install, came on June 10. Talk to our team to put the same gate in front of your supply chain.
References
- StepSecurity, The Hades Campaign: Malicious PyPI Packages
- Socket, Mini Shai-Hulud, Miasma, and Hades Worms Target Bioinformatics and MCP Developers
- Socket, Shai-Hulud Descends to Hades: Miasma Worm Campaign Spreads with New PyPI Wave
- MITRE ATT&CK, T1195.001 Compromise Software Supply Chain
- CyberXYZ Threat Intelligence, internal detection telemetry, June 2026