PyPI

The pystr-codec PyPI Worm

Updated 31 Oct 2026 · Incident date 31 Oct 2026

pystr-codec version 0.1.1 on PyPI adds a .pth file (a Python startup helper) that runs at every Python start. It downloads the Bun JavaScript runtime and runs a hidden 17 MB script. The script collects developer keys and tokens, then uses them to publish poisoned releases of the victim's own packages.

Vigilance flagged the release on 31 October 2026, 12 minutes after the wheel went up, as one package that gained a startup hook. We reported it to the PyPI security team the same evening. The full analysis is in VIGI-002-2026. As of 31 October 2026, 23:13 UTC, PyPI still served the package, and OSV had no entry for it.

What happened

pystr-codec 0.1.1 shipped two files that 0.1.0 did not carry: pystr_codec-setup.pth and pystr_codec/ci.js, a 17,908,149-byte script. Version 0.1.0 went up at 22:21 UTC and 0.1.1 at 22:46 UTC on the same day.

Python runs every line in a .pth file that starts with import, at the start of every Python process. The line in this file runs once per machine. It looks for ci.js on the Python path and downloads Bun 1.2.18 from Bun's official GitHub releases into the temp folder. Then it runs bun run ci.js and writes a marker file named .bun_ran so it does not run again.

We decoded ci.js offline in a locked container and did not run it. It has three layers:

  1. A hex string, shifted by a letter cipher, then passed to eval.
  2. 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, piped into a new Bun process.
  3. A bundle scrambled with a common JavaScript obfuscator.

The text inside the last layer names the parts of a worm:

  • A collector that reads ~/.ssh, ~/.aws, ~/.kube, ~/.npmrc, ~/.pypirc and the keychain. It tests GitHub, GitLab, npm, PyPI, RubyGems and JFrog tokens for write access.
  • A Google Cloud part that lists projects and reads Secret Manager secrets.
  • A PyPI publisher that uses a stolen token, or makes one from a GitHub Actions identity token. It names the -setup.pth file pattern, which matches the loader in this package.
  • A GitHub part that adds workflows to writable repositories and force-pushes them, to publish through npm.
  • An Artifactory part that replaces cached npm packages.
  • A provenance part that makes Sigstore entries, so the poisoned releases carry a signature record.
  • A dispatcher that sends the collected results out in batches.

Affected versions

  • pystr-codec 0.1.1 carries the .pth file and ci.js. The only build is pystr_codec-0.1.1-py3-none-any.whl.
  • 0.1.0 has neither file.
  • The loader has branches for macOS, Linux and Windows, on x64 and arm64.
  • Any package that a victim's stolen token can publish to can carry the same loader in a later release.

The same family

The 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.

pystr-codec uses the same .bun_ran marker and the same b/bun path. It differs in three ways. 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. We are the first to report pystr-codec. We are not the first to report this worm. The full analysis is in VIGI-002-2026.

Indicators of compromise

  • Package: pystr-codec 0.1.1 on PyPI
  • Wheel SHA-256: 69c812d101933f0c6a31bb6f23d619b0826fd59a6866a26f6ea2fc031be60241
  • Startup file: pystr_codec-setup.pth, 1,391 bytes
  • Payload: pystr_codec/ci.js, 17,908,149 bytes
  • Files in the temp folder: .bun_ran, b/bun (or b\bun.exe), b.zip
  • Download: github.com/oven-sh/bun/releases/download/bun-v1.2.18/ from a Python process
  • Pattern: a package that gains a <name>-setup.pth file next to a large ci.js

How to check

Look for the package first.

pip show pystr-codec

Then search every Python environment for a startup file with this name pattern. The worm can plant it in other packages.

find / -name '*-setup.pth' 2>/dev/null

On macOS and Linux, check the temp folder for the marker file and the Bun copy.

ls -la "$(python3 -c 'import tempfile; print(tempfile.gettempdir())')"/.bun_ran "$(python3 -c 'import tempfile; print(tempfile.gettempdir())')"/b

On Windows, the same files sit in %TEMP%.

dir /a "%TEMP%\.bun_ran" "%TEMP%\b"

If .bun_ran exists, the loader ran on that machine at least once.

What to do now

If pystr-codec 0.1.1 ran on a machine, treat every key and token on that machine as stolen.

  1. Uninstall the package: pip uninstall pystr-codec.
  2. Delete pystr_codec-setup.pth from site-packages if the uninstall leaves it behind.
  3. Delete .bun_ran, b and b.zip from the temp folder.
  4. Rotate SSH keys, cloud keys, Kubernetes configs, and the npm, PyPI, RubyGems, GitHub, GitLab and JFrog tokens.
  5. Read the release history of every package those tokens can publish. Look for a new release you did not make.
  6. Read the workflows in every repository the GitHub token can write to. Look for a new workflow or a force-push you did not make.
  7. If the machine had Google Cloud access, read the Secret Manager access log.

What Vigilance would show (rebuilt from public reports)

Vigilance compares the version you trust with the new one and names the file that gained a new power. The block below is rebuilt from the public reports in the words Vigilance prints. It is not a captured scan, because the malicious release is not redistributed.

vigi diff --old pystr-codec-0.1.0 --new pystr-codec-0.1.1
files scanned: 33

HEADS UP  1 file can now do things the old version could not. The rest changed and gained nothing.

CHANGED    pystr_codec-setup.pth
           It now downloads from the internet, reads saved passwords and access keys and runs other programs. It did not before.

Frequently asked questions

What does the pystr-codec package do?

Version 0.1.1 adds a Python startup file that downloads the Bun runtime and runs a hidden script. The script collects keys and tokens and uses them to publish poisoned releases of other packages.

Is pystr-codec 0.1.0 safe?

Version 0.1.0 does not carry the startup file or the script. It went up 25 minutes before 0.1.1 from the same account, so remove both.

How did Vigilance find pystr-codec?

Vigilance compares each new release with the one before it and flags a file that gained a new power. It flagged 0.1.1 for a new startup hook 12 minutes after the wheel went up.

What is a .pth file?

A text file that Python reads at startup to extend the module search path. Python runs any line in it that starts with import, on every Python start, with no click.

Sources

  1. pypi.org/project/pystr-codec/0.1.1/
  2. pypi.org/pypi/pystr-codec/json
  3. socket.dev/blog/shai-hulud-descends-to-hades-miasma-pypi-wave
  4. files.pythonhosted.org/packages/0c/7a/267aa3b1671e0437ce0d5ec3c5931e451ae1c81b170602ecd025f6d889e3/pystr_codec-0.1.1-py3-none-any.whl

More supply chain attacks

All 124 attacks in the library · What is a supply chain attack? · How to prevent supply chain attacks

Check the next update before you install it

Vigilance compares the version you trust with the new one. It names the one file that gained a power the old version did not have.

Start the free 14-day watchBrowse the attack library