Ten malicious versions of the npm package @7nohe/openapi-react-query-codegen appeared in about 20 minutes on August 28. JFrog Security Research found that an untrusted pull-request author could post one specific comment, make the project’s release workflow publish the pull-request checkout, and give the resulting packages genuine GitHub Actions trusted-publishing provenance.
Eight stable releases carried Trinitite, a Mini Shai-Hulud worm that steals developer, registry, cloud, Vault, Kubernetes, SSH and CI credentials. It can use that authority to republish other writable packages. The code generator records more than 150,000 weekly downloads, although JFrog did not quantify installs of the malicious versions, successful theft, downstream package infection or file deletion.
One comment published ten releases
The project workflow treated a pull-request comment containing exactly npm publish as a release trigger. It did not verify that the commenter was a maintainer. The job checked out the contributor’s pull request and requested an OpenID Connect token with id-token: write, then used npm trusted publishing to release that checkout.
GitHub user p00paboot opened the pull requests and posted the trigger, according to JFrog. The provenance attached to the packages confirms that the repository job ran. It cannot establish that the checked-out code was clean. JFrog captured the distinction in one line: “Valid provenance on these versions is not a clean bill of health.”
The first two releases were prereleases. Version 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be added a preinstall command that downloaded the official Bun installer from the legitimate shared service raw.githubusercontent.com, set WORKFLOW_ID=release.yml, REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen and TARGET_PACKAGES=@7nohe/openapi-react-query-codegen, then tried to run the absent file is_it_this_simple.js. JFrog says that prerelease did not drop the worm because its tarball lacked both the loader and binding.gyp.
Two install paths launch Trinitite
Wave 1 comprised versions 0.5.4, 1.6.3, 2.2.1 and 3.0.3. Each carried binding.gyp and the 4–6 MB XOR-wrapped loader 3FWCvzduYZg.js. The GYP file hid a Python expression in Unicode escapes. During evaluation, it walked Python’s class tree to catch_warnings, reached __builtins__, imported os and ran node 3FWCvzduYZg.js.
That path matters because a control focused on package lifecycle scripts can miss it. node-gyp can evaluate binding.gyp during installation even when --ignore-scripts suppresses a package’s preinstall hook. Wave 2 (versions 0.5.5, 1.6.4, 2.2.2 and 3.0.4) kept the GYP route and added the direct hook preinstall: node 3FWCvzduYZg.js.
The loader decodes two AES-128-GCM blobs. One obtains Bun; the other is the worm. If Bun is absent, the installer retrieves the official runtime from GitHub’s legitimate release infrastructure and stages it at /tmp/trinnyyyy-*/bun on Unix-like systems or under a random six-character filename on Windows. The normal user path ~/.bun/bin/bun is another legitimate runtime location abused by the package.

Figure details
An untrusted pull-request author posts the specific release comment. GitHub Actions checks out the pull request and uses OpenID Connect trusted publishing to issue malicious npm versions with valid provenance. At install time, a package.json preinstall hook and a Unicode-obfuscated binding.gyp evaluation converge on 3FWCvzduYZg.js. The loader starts Trinitite, which steals credentials and uses writable package authority to publish further releases.
Stolen authority drives propagation
Once running, the worm searches for GitHub, npm, PyPI, RubyGems, cloud, SSH, Vault, Kubernetes and CI material. It also scrapes GitHub Actions Runner.Worker memory for "isSecret":true. JFrog says the payload validates stolen PyPI tokens against the legitimate endpoint upload.pypi.org, checks package authority at registry.npmjs.org, and uses legitimate Sigstore services at fulcio.sigstore.dev and rekor.sigstore.dev during trusted publishing.
Stolen data is compressed, encrypted and committed to a public repository through the legitimate api.github.com service. Result filenames begin doubletrinnys-. A commit without a token uses meow meow meow; one carrying a token starts IfYouRevokeThisTokenYourABadUser. The new campaign description is Trinitite: Sponsored by Preview 2 Effects, and the planted secret-dump workflow is named ClaudeCode Review.
Persistence spans Linux and macOS user services plus development-tool hooks. The Python monitor lives at ~/.local/share/diaper/poopy.py, with state in /var/tmp/.shit. Linux service files are ~/.config/systemd/user/systemd-detect-fash.service and ~/.config/systemd/user/sysvinit-detect-fash.service; related configuration sits under ~/.config/sysvinit-detect-fash/. macOS uses ~/Library/LaunchAgents/com.user.systemd-detect-fash.plist or ~/Library/LaunchAgents/com.user.sysvinit-detect-fash.plist.
The monitor polls the GitHub user endpoint. JFrog found a stored handler that can erase files under the user’s home directories when a 40x response indicates that a token has died. This makes the order of response operations consequential.
New strings expose infected hosts
Hunt installed package trees and build caches for 3FWCvzduYZg.js, binding.gyp and the referenced is_it_this_simple.js. Search endpoints for /tmp/trinnyyyy-*/bun, ~/.bun/bin/bun, /var/tmp/.shit, the monitor path, both Linux service files, the sysvinit configuration directory and both macOS LaunchAgent files. Review user-level services and development-tool startup hooks for unexpected execution.
Print every campaign marker in searches: Trinitite: Sponsored by Preview 2 Effects, doubletrinnys-, meow meow meow, IfYouRevokeThisTokenYourABadUser, Visit69WykenAveForFreeiPod, n1ggatr1n, StopRapingMyBotnetPlz and ClaudeCode Review. The payload also contains the decoy poopy[.]com/v1/idk; JFrog did not observe that attacker-controlled host used. Treat traffic to the legitimate shared services named above as context that requires process and package correlation, rather than a malicious verdict by hostname.
The last-safe versions are 0.5.3, 1.6.2, 2.2.0 and 3.0.2. JFrog lists the affected releases as both prereleases plus 0.5.4, 0.5.5, 1.6.3, 1.6.4, 2.2.1, 2.2.2, 3.0.3 and 3.0.4. JFrog is evaluating its own products when it says Xray and Curation identify them under XRAY-1065308.
JFrog does not attribute the operator. It notes that Australian authorities arrested suspected TeamPCP members roughly a day before the npm releases, while new RSA keys and campaign strings leave open whether the wave came from residual access or another operator using the same kit. The maintainer’s, npm’s and GitHub’s responses were not disclosed in the primary report.
Isolate hosts before rotating tokens
Isolate any workstation or runner that installed a listed version with lifecycle scripts or allowed node-gyp to evaluate the planted GYP file. Stop and disable the two user services, unload either LaunchAgent, remove the Python monitor and its state, and preserve package trees, lockfiles, process history, user-service state, development-tool hooks, repository activity, Actions logs and registry publication records before cleanup.
Rotate GitHub, npm, PyPI, RubyGems, cloud, SSH, Vault, Kubernetes and CI credentials from a clean system only after the monitor and persistence are gone. Rebuild from a verified last-safe version and regenerate lockfiles. Also repair release workflows so an untrusted pull-request comment cannot start publishing, pin external Actions by commit, constrain id-token: write, and require an authorized maintainer or protected environment to approve a release.
Verify the rebuilt host by resolving every installed copy of @7nohe/openapi-react-query-codegen, inspecting its lockfile entry and searching the preserved filesystem for the published files, paths, services and strings. The expected result is a last-safe version, no malicious loader or persistence artifact, and no unexplained package publication or public result repository tied to the rotated identities.
Trusted publishing authenticated the release machinery while the workflow supplied attacker-controlled code. Recovery therefore has two boundaries: the compromised host and the automation that granted it distribution authority. Both must be made defensible before the same provenance signal can carry trust again.
