- Package
- pystr-codec
- Bad version
- 0.1.1
- Registry
- PyPI
- Status
- Live on PyPI
- First seen
- 31 Oct 2026, 22:46 UTC
- Flagged
- 31 Oct 2026, 22:58 UTC
The short version
If you install pystr-codec 0.1.1, Python runs its startup file every time Python starts. Once per machine, that file downloads Bun from Bun’s official GitHub releases. Then it runs a 17 MB script that ships inside the package.
The script reads the keys and tokens that a developer keeps on the machine. It checks which ones can publish code. It then uses them to publish new releases of the victim’s own packages and to add workflows to the victim’s GitHub repositories.
Nothing runs during pip install. The code runs at the next Python start.
Timeline
- Version 0.1.0 goes up on PyPI. It carries the library code only.
- Version 0.1.1 goes up on PyPI, 25 minutes later. It adds the startup file and the 17 MB script.
- Vigilance flags it. The watcher compares 0.1.0 against 0.1.1 and sees a file that runs at every Python start. The old version had no such file. Twelve minutes after upload, with no signature and no prior report of this package anywhere.
- We decode the script offline and report the package to the PyPI security team.
- PyPI still serves the package. OSV has no record for it.
What the update changed
Version 0.1.1 adds two files to the one wheel the package ships. Every other file stays the same, apart from the package metadata.
pystr_codec-0.1.0 → 0.1.1 py3-none-any + pystr_codec-setup.pth 1,391 bytes + pystr_codec/ci.js 17,908,149 bytes
Python reads every .pth file in site-packages at start. It runs any line
that starts with import. This file holds one such line. Shortened, it does this:
if no file named .bun_ran in the temp folder:
find ci.js on the Python path
download bun-v1.2.18/bun-<os>-<arch>.zip from github.com/oven-sh
unpack bun into <temp>/b/
run: bun run ci.js
create <temp>/.bun_ran
What the script does
We decoded ci.js offline in a locked container. It has three layers:
- A hex string, shifted by a letter cipher, then passed to
eval. - Two AES-128-GCM blobs, with the keys in the file. One is a helper that downloads Bun. The other is a 10.96 MB compressed script that runs in a new Bun process.
- A bundle scrambled with a common JavaScript obfuscator. Its text table decodes with a changed base64 alphabet.
The decoded text names each part of the program in its log lines. These are the parts:
| Collector | Reads ~/.ssh, ~/.aws, ~/.kube, ~/.npmrc, ~/.pypirc and the keychain. Tests GitHub, GitLab, npm, PyPI, RubyGems and JFrog tokens for write access. |
|---|---|
| Google Cloud | Lists projects and reads secrets from Secret Manager. |
| PyPI | Publishes with a stolen token, or exchanges a GitHub Actions identity token for a PyPI token. It names the -setup.pth file pattern that this package uses. |
| GitHub and npm | Lists writable repositories, adds workflows and force-pushes them, to publish through npm. |
| Artifactory | Replaces cached npm packages in a JFrog registry. |
| Provenance | Creates Sigstore and Rekor entries for the releases it publishes. |
| Dispatcher | Sends the collected results out in batches. |
We read the text of the program. We did not run it, so we did not see where the dispatcher sends its results.
The same family
This loader matches the “Hades” wave of the Mini Shai-Hulud and Miasma worm.
Socket
reported that wave in June 2026: 37 wheels across 19 PyPI packages, each with a
*-setup.pth file that downloads Bun and runs an obfuscated script.
The Hacker News
and GitLab
covered it too.
| Same | The -setup.pth name, the .bun_ran marker, the <temp>/b/bun path, the ROT and AES-128-GCM layers, and the list of secrets. |
|---|---|
| Different | The script is ci.js, not _index.js. The loader downloads Bun 1.2.18, not 1.3.13. It searches the whole Python path for the script. The main stage goes to Bun through standard input, not a file in /tmp. |
We are the first to report pystr-codec. We are not the first to report this worm.
What the records showed
On 31 October 2026 at 23:13 UTC, OSV had no record for pystr-codec. Nothing runs during
pip install, so an install-time sandbox sees a clean package. The payload
sits under three layers of encoding and encryption, so a text search of the package
finds no address and no secret path.
Who is behind it
The package metadata names no author, no home page, no licence and no project links. Both versions went up on the same day, 25 minutes apart. We did not identify the PyPI account behind it.
If you ran this
This applies to anyone who installed pystr-codec 0.1.1 and then started Python.
- Uninstall the package:
pip uninstall pystr-codec. - If
pystr_codec-setup.pthis still in site-packages, delete it. - Delete
.bun_ran,bandb.zipfrom the temp folder. -
Rotate your credentials from a machine you trust:
- SSH keys
- AWS, Google Cloud and Kubernetes credentials
- npm, PyPI, RubyGems and JFrog tokens
- GitHub and GitLab tokens
- Read the release history of every package your tokens can publish. Look for a release you did not make.
- Read the workflows in every repository your GitHub token can write to. Look for a workflow or a force-push you did not make.
Indicators
| Package | pystr-codec 0.1.1 on PyPI |
|---|---|
| Wheel | sha256 69c812d101933f0c6a31bb6f23d619b0826fd59a6866a26f6ea2fc031be60241 |
| Startup file | pystr_codec-setup.pth, 1,391 bytes |
| Script | pystr_codec/ci.js, 17,908,149 bytes |
| Decoded script | sha256 cae101cfa0a2771e3ed75575f95011f56d04d8e70d7c86169be4fc24aca9f80e |
| Files dropped | <temp>/.bun_ran, <temp>/b/bun or bun.exe, <temp>/b.zip |
| Download | https://github.com/oven-sh/bun/releases/download/bun-v1.2.18/, fetched by a Python process |
| Pattern | A package that gains a <name>-setup.pth file beside a large ci.js |
Disclosure
Reported to the PyPI security team on 31 October 2026. We executed none of the malicious code. Everything here comes from the package files and the public PyPI records.
If you are the maintainer and you believe this is wrong, write to support@vigihq.com and we will publish a correction on this page.
VIGI-002-2026. Published 31 October 2026 by Vigilance. On that date PyPI still served version 0.1.1. If PyPI removes the package, we will record the time here.