Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
Ten malicious versions were published with valid npm provenance after a threat actor abused a -triggered GitHub Actions publishing workflow, with the latest release still compromised at the time of writing.
The Socket Threat Research Team is investigating an ongoing Mini Shai-Hulud compromise, affecting the npm package @7nohe/openapi-react-query-codegen . On August 28, 2026, ten malicious versions were published in two waves roughly twenty minutes apart, spanning every maintained release line. At the time of writing, all ten remain installable and the latest tag resolves to malicious version 3.0.4 . The package receives roughly 1 50,000 weekly downloads across all versions.
The compromised releases execute threat actor-controlled code during installation. A bundled, obfuscated JavaScript loader decrypts and executes a second-stage payload that targets cloud credentials, package registry credentials, GitHub Actions secrets, and AI agent configuration. The payload also contains self-propagation functionality consistent with Mini Shai-Hulud. The Socket Threat Research Team is continuing to analyze the payload, persistence mechanisms, and broader scope of the compromise.
Notably, all ten malicious versions carry valid npm provenance attestations issued through GitHub Actions trusted publishing. Our investigation found that a -triggered publishing workflow can be invoked by an untrusted GitHub account to publish code from a pull request fork under the repository’s trusted publishing identity.
This is an ongoing investigation. The Socket Threat Research Team is continuing to track the Mini Shai-Hulud compromise and analyze the malicious payloads, publishing workflow abuse, and broader scope of activity. We will continue to update this post with additional findings and technical analysis as the investigation evolves.
Compromised Packages and Versions #
The ten compromised versions of @7nohe/openapi-react-query-codegen are listed below:
@7nohe/openapi-react-query-codegen@0.0.0-365d4eb738d3146583431948d3ba6e27a32556be
@7nohe/openapi-react-query-codegen@0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab
@7nohe/[email protected]
@7nohe/[email protected]
@7nohe/[email protected]
@7nohe/[email protected]
@7nohe/[email protected]
@7nohe/[email protected]
@7nohe/[email protected]
@7nohe/[email protected]
The attacker republished across every maintained release line. Last known-good version per line: 0.5.3 , 1.6.2 , 2.2.0 , 3.0.2 .
Ten versions of the npm package @7nohe/openapi-react-query-codegen were published with malicious code on August 28, 2026, in two waves roughly twenty minutes apart. All ten remain installable at the time of writing, and the latest tag still resolves to the malicious 3.0.4 — a plain npm install [@7nohe/openapi-react-query-codegen]( ) currently retrieves a compromised build.
The malicious releases execute a bundled loader, 3FWCvzduYZg.js , at install time. The loader is a ~5.7 MB single-byte-XOR-obfuscated file that decrypts an embedded AES-128-GCM payload, writes it to a random filename in the temp directory, executes it via child_process.execSync , and unlinks it in a finally block.
Our subsequent analysis decrypted the second-stage payload, which targets cloud credentials, package registry credentials, GitHub Actions secrets, and AI agent configuration, and contains self-propagation functionality. Analysis of additional payload functionality and persistence mechanisms remains ongoing.
All ten versions carry valid npm provenance attestations issued through GitHub Actions trusted publishing.
Wave-1 versions execute only through binding.gyp , which is identical across those versions and uses an obfuscated Python expression to reach os.system and run node 3FWCvzduYZg.js . Because binding.gyp is processed by node-gyp during installation, this fires on npm install in developer environments and CI runners.
Wave-2 manifests carry the same binding.gyp and additionally add "preinstall": "node 3FWCvzduYZg.js" , giving the attacker a second, more reliable execution trigger.
The two 0.0.0-* prereleases use different execution paths.
0.0.0-365d4eb738d3146583431948d3ba6e27a32556be uses a preinstall script that fetches and runs the Bun installer, then executes is_it_this_simple.js with WORKFLOW_ID=release.yml , REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen , and TARGET_PACKAGES=@7nohe/openapi-react-query-codegen in the environment.
0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab follows a separate path. Its preinstall sets the same environment variables but executes node nu.js directly and does not fetch the Bun installer.
The threat actor staged these paths across separate commit chains in the fork. One chain adds is_it_this_simple.js before introducing the Bun-based preinstall ; the other adds nu.js before introducing and subsequently modifying the corresponding preinstall .
Provenance Does Not Help Here #
All ten malicious versions carry valid npm provenance attestations issued through GitHub Actions trusted publishing, using the same OIDC configuration as the legitimate 2.2.0 and 3.0.2 releases. Their SLSA predicates name .github/workflows/release.yml on refs/heads/main at resolved commit d42d1733 — the clean v3.0.2 commit — from two Actions runs.
The attestations verify successfully while the tarballs contain attacker code. npm audit signatures will not flag these versions. Provenance proves which workflow built an artifact; it does not prove that the workflow only builds trusted source.
There is no pull_request_target workflow in the development repository. release.yml triggers on issue_comment: [created] and gates only on the text of the :
The job then checks out the pull request head from the fork ( pull/ /head ), installs dependencies, and runs pnpm publish --no-git-checks with id-token: write .
The commenter's repository association is never checked. Any GitHub account can publish a fork's contents under this repository's OIDC identity by commenting npm publish on any pull request. And because GITHUB_REF for issue_comment events is the default branch, the resulting provenance records refs/heads/main at the last legitimate commit — which is exactly why the attestations look clean.
For users of the package:
Immediately isolate any machine or CI runner that installed an affected version from the network and treat the environment as compromised.
Prefer rebuilding affected developer systems and CI runners from a known-clean image. If in-place remediation is necessary, identify and remove malicious persistence before revoking exposed credentials.
After the affected environment has been contained, rebuilt, or its persistence neutralized, revoke and rotate all credentials that were accessible from it, including npm tokens, GitHub tokens, cloud credentials, package registry credentials, and CI secrets.
Pin @7nohe/openapi-react-query-codegen to a known-good version — 0.5.3 , 1.6.2 , 2.2.0 , or 3.0.2 — and reinstall from a clean lockfile. Clear package-manager caches and remove existing node_modules so a malicious tarball is not reused.
Check lockfiles and SBOMs for all ten affected versions, including transitive installations ( npm ls @7nohe/openapi-react-query-codegen ). Do not rely on npm audit signatures alone: the malicious releases carry valid npm provenance attestations.
For maintainers running -triggered publish workflows:
Any workflow that publishes on issue_comment and gates only on text will publish anything a stranger puts in a fork. Verify the commenter's repository association ( github.event. .author_association ) before running a job that holds id-token: write , or move publishing to a trigger that cannot be fired by an untrusted account.
The npm package contains a 5.6 MB single-line JavaScript payload ( package/3FWCvzduYZg.js ) and a malicious binding.gyp . The binding.gyp condition abuses Python object traversal and os.system() to launch that payload during the native-build path:
The escaped strings resolve to catch_warnings , __import__ , os , and node 3FWCvzduYZg.js . The payload then statically embeds and deploys multiple secondary modules. Its capabilities include:
credential discovery across source files, process memory, cloud metadata services, and CI/CD variables;
validation and use of GitHub, npm, PyPI, RubyGems, and JFrog credentials;
encrypted collection exfiltration through attacker-created public GitHub repositories;
npm/JFrog/RubyGems package poisoning and optional PyPI typosquatting;
GitHub Actions workflow modification and deployment-triggered secret collection;
persistent GitHub-token monitoring on macOS and Linux;
persistence in developer-tool settings and MCP-related configuration;
signed, covert remote-command retrieval through GitHub commit messages; and
SSH-based propagation to reachable hosts.
The analysis below is based on archive listing, extraction, deobfuscation, and source inspection only. The npm package, JavaScript payload, Bun binary, shell modules, and Python modules were not executed.
Attack Chain Summary #
Stage 1: Malicious Build Trigger Loads Root Payload #
The tarball contains a native-build manifest even though the package has no native implementation. Its binding.gyp uses a Python catch_warnings subclass lookup to reach Python builtins, import os , and invoke os.system() . The final escaped command is node 3FWCvzduYZg.js .
This is a deliberately indirect execution path: the command is hidden in Unicode escapes, and the conditions expression is evaluated by the build tooling rather than exposed as an obvious npm lifecycle script. The payload file is 5,616,000 bytes, has no line terminators, and begins with an XOR-decoding Function constructor:
The payload contains a large string table and multiple compressed or encoded module bodies. Static extraction recovered shell, JavaScript, and Python modules implementing the behaviors described below.
Stage 2: Bun Loader Downloads and Loads Secondary Modules #
The first recovered loader is a platform-aware shell script. It exits if Bun is already on PATH ; otherwise it selects a Bun 1.4.0 asset for Linux or macOS and x64 or arm64, downloads it from GitHub, extracts it, and executes ai_init.js :
A second Node loader performs the same role with an obfuscated string table. It downloads an architecture-specific Bun archive from github.com/oven-sh/bun/releases/download/bun-v1.4.0/ , extracts Bun, and runs a payload entry script. It uses execFileSync , execSync , https.get , filesystem writes, and a detached child process.
The main payload contains a relaunch guard. If __DOGINSIDEPC is absent, it starts a detached copy of the current process with that environment variable set, ignores standard input/output/error, and exits the parent after confirming the child remains alive. This hides the active process from the invoking build command and keeps execution independent of the initial process lifetime.
LaunchAgent and Service Persistence on MacOS and Linux #
A separate token-monitor installer writes these paths:
On macOS it creates and bootstraps a LaunchAgent with RunAtLoad and KeepAlive . On Linux it creates and enables a user systemd service and enables lingering. The monitor polls GitHub once per minute for up to 259,200 seconds (72 hours despite the log text saying “24h TTL”), checks with a stored bearer token, and executes the stored handler when GitHub returns an HTTP 4xx response:
The use of eval turns the handler stored in fash-detected into executable shell code.
Local, Memory, Cloud, and CI Credential Discovery #
The main dispatch function calls three discovery providers before credential-specific providers:
Filesystem Scanning #
The filesystem provider recursively scans the current working tree, including dotfiles, and applies credential regular expressions. The default patterns include:
The filesystem provider also searches for broader secret classes, including JWTs, connection strings, and cloud credentials. It can inspect up to 12,000 files in its recursive walk.
Process-Memory Scanning #
The recovered Python memory dumper reads /proc/ /maps and /proc/ /mem , selects readable mappings, seeks through each range, and writes the bytes to stdout:
The JavaScript provider launches this helper against candidate processes, then scans the resulting memory dump for the same credential patterns. This is intended to recover secrets held by running developer tools, package managers, CI agents, and other processes even when they are not present on disk.
Environment and Cloud Metadata #
The environment provider enumerates a broad CI/CD and cloud variable set, including GITHUB_TOKEN , ACTIONS_ID_TOKEN_REQUEST_TOKEN , ACTIONS_ID_TOKEN_REQUEST_URL , CI_JOB_TOKEN , GITLAB_TOKEN , NPM_TOKEN , ANTHROPIC_API_KEY , AWS_ACCESS_KEY_ID , AWS_SECRET_ACCESS_KEY , AWS_SESSION_TOKEN , AWS_WEB_IDENTITY_TOKEN_FILE , AZURE_CLIENT_ID , AZURE_CLIENT_SECRET , AZURE_FEDERATED_TOKEN_FILE , AZURE_TENANT_ID , GOOGLE_APPLICATION_CREDENTIALS , GOOGLE_CLOUD_PROJECT , VAULT_TOKEN , VAULT_ADDR , VERCEL , BUILD_NUMBER , and many CI-provider-specific variables.
Cloud-provider modules query credential endpoints, including:
The AWS module requests an IMDSv2 token, reads instance-role credentials, and also handles web-identity credentials. Azure modules resolve tenant, object, client, subscription, and display-name data from tokens and Microsoft APIs. The code also contains provider logic for Google, Vault, and other cloud credentials.
Credential Validation and Capability Selection #
The payload does not indiscriminately exfiltrate every regex match. It validates credentials and selects follow-on actions based on their scopes and permissions.
For GitHub tokens, it queries the GitHub API, records scopes and user/org metadata, and enables repository actions only for tokens with repo or public_repo scope:
For npm tokens, it queries , requires a non-2FA token with package write permission, calls /-/whoami , and enumerates writable organization or package scopes. For PyPI tokens, it sends a multipart upload probe to ; a 400 response is treated as evidence that the token is valid, after which the code probes package ownership. RubyGems tokens are checked through and require the index_rubygems scope. JFrog credentials are tested against discovered .jfrog.io endpoints and used only when the session reports write access.
Collection and Encrypted Exfiltration #
The payload batches successful findings in an ingestion buffer. The default flush threshold is 0x19000 bytes (102,400 bytes):
The envelope implementation serializes the collected JSON, compresses it with gzip, generates a random 32-byte AES key and 12-byte nonce, encrypts the compressed data with AES-256-GCM, and RSA-OAEP encrypts the AES key using an embedded public key:
This design hides collection contents from casual inspection while using a legitimate developer platform as the transport and storage layer. The payload also contains an embedded public key in the Python monitor and a corresponding obfuscated key in the JavaScript code. The public key artifact begins:
Package Poisoning and Registry Abuse #
When a validated npm token has package write access, the payload enumerates writable packages and downloads their tarballs. The JFrog path similarly resolves writable npm repositories, downloads package tarballs, modifies them, and republishes them. The JFrog package processor calls a tarball modifier with the explicit tag [jfrognpm] and addBunDep: true :
The npm publisher invokes the registry publish path with the validated auth token. This gives the attacker a mechanism to add a loader to packages under the victim’s namespace or private registry scope.
The RubyGems path enumerates gems owned by a validated account, downloads each gem, modifies it with the current payload, and submits it through the RubyGems API:
The validator first requests and requires index_rubygems in the returned scopes.
The PyPI path contains a conditional typosquatting mode. It only executes when TYPO_MODE === '1' and TARGET_PACKAGES is populated. For each target it generates up to 20 suffix variants using -mcp and -mpc , skips existing names, injects a Python payload, and uploads the resulting wheel with the stolen token. The fallback Python payload is explicit:
The normal path can instead carry a supplied JavaScript or Python payload into generated wheels. The presence of this gated mode indicates an operator-controlled expansion path rather than a benign package-management feature.
GitHub Actions Workflow Poisoning #
A dedicated GitHub module enumerates repositories where the token has push access, inspects workflow files, and identifies workflows that can deploy. It rewrites the workflow and adds .github/_index.js through GitHub’s Git object and commit APIs. The standard path creates a commit with the message chore: update dependencies , then a second commit with [skip ci] , creates a deployment, and deletes the deployment afterward.
The recovered workflow payload is named ClaudeCode Review and runs on the deployment event. It serializes all repository secrets into an environment variable and writes them to res.txt before uploading the file as an artifact:
A second recovered VS Code task payload executes node .claude/setup.mjs on folderOpen . Together, these payloads provide both CI secret collection and developer-workstation execution paths.
The module also supports a branch-push fallback that commits the modified workflow and _index.js directly when deployment-based execution is unavailable. This is repository tampering, not normal dependency automation.
Developer-Tool Persistence and Remote Command Execution #
The payload searches -directory trees for files named settings.json , config.json , mcp.json , and .aider.conf.yml , plus .cursorrules , .windsurfrules , .cursor/rules/ , and .github/copilot-instructions.md . It filters paths containing developer-tool names such as claude , codex , gemini , copilot , kiro , opencode , cline , aider , tabby , amazonq , cody , bolt , and continue .
For each writable configuration it creates .config/index.js , copies the current payload into that file, and injects a background command that runs it at session start:
JSON and YAML configurations receive structured hook modifications; text rule files receive the hidden -and-command form. This persistence targets AI coding assistants and related developer tooling, allowing future sessions to re-run the payload without reinstalling the npm package.
The recovered primary credential harvester polls GitHub commit once per hour for the query firedalazer . It parses commit messages matching:
It verifies the signature using RSA-PSS/SHA-256 and the embedded public key, downloads the URL, writes it to a temporary Python file, executes it with python3 , and records the URL hash in /var/tmp/.shit to avoid repeating commands:
This is a covert signed command channel. The commit message carries both the command location and its signature, while the public GitHub API supplies a low-friction rendezvous point.
SSH Propagation and Anti-Analysis Controls #
The SSH propagator gathers candidate hosts from local SSH configuration and known-host data, then uses noninteractive SSH with host-key checking disabled:
It copies ai_setup.sh and ai_init.js with scp , runs bash ai_setup.sh remotely, and starts the implant in the remote environment. The recovered scripts are designed for macOS and Linux and download Bun when necessary.
The payload includes explicit anti-analysis behavior:
checks for bun and exits or changes loader behavior when it is already installed;
checks GITHUB_ACTIONS , workflow identifiers, repository suffixes, and execution context;
exits when anti-malware or security-tool detection is present;
uses a detached relaunch and the __DOGINSIDEPC guard;
suppresses errors across most collection and propagation operations;
uses random temporary directories and random repository names; and
contains code that evaluates a GitHub commit-based anti-analysis/deactivation path.
A notable anti-analysis guard looks for a known file or command output before changing its selected payload behavior. The code’s breadth of guards and quiet failure handling is consistent with an implant intended to survive heterogeneous developer and CI environments.
Campaign Similarities #
The artifact overlaps materially with public descriptions of both the 2025 Shai-Hulud npm worm and the later Mini Shai-Hulud campaign. Microsoft describes Mini Shai-Hulud as compromised npm packages that execute during npm install , install or use Bun, scrape GitHub Actions Runner memory, steal GitHub/AWS/Vault/Kubernetes/npm credentials, and abuse CI/CD environments . The same broad pattern—credential theft enabling supply-chain propagation and CI/CD compromise—is characteristic of public reporting on Shai-Hulud, but behavioral overlap alone does not establish shared operators or campaign identity.
Observed overlap in this sample:
npm supply-chain delivery with execution during an installation/build path;
a large obfuscated JavaScript bundle and a Bun-based secondary loader;
broad credential harvesting from environment variables, files, process memory, and cloud metadata;
GitHub Actions workflow modification to expose repository secrets;
validation and reuse of stolen npm and GitHub permissions;
self-propagating package poisoning through writable maintainer or registry scopes; and
GitHub repositories used for operations, storage, or command exchange.
Important differences prevent attribution based on behavior alone. This sample has a Unicode-escaped binding.gyp trigger rather than the postinstall pattern described in those campaign reports; it supports macOS as well as Linux; it uses encrypted AES-GCM/RSA envelopes and attacker-created public repositories; and it adds RubyGems/JFrog abuse, gated PyPI typosquatting, developer-tool configuration persistence, a signed GitHub-commit command channel, and SSH propagation. These are technical similarities and differences, not proof that the package belongs to either named campaign. The package-specific hashes, strings, paths, and endpoints in this report should be used for detection.
Additional Response Guidance #
Treat installation or execution of this package as a potential host compromise. Because the installed monitor evaluates its destructive handler when the monitored GitHub token returns status 400–409, contain the host before revoking or testing that token. From a trusted management plane, isolate the host from the network and terminate the payload and its persistence mechanisms; if that is not possible, power it down to prevent the monitor from receiving the triggering response.
Before credential revocation, disable the LaunchAgent com.user.sysvinit-detect-fash , the systemd user service sysvinit-detect-fash.service , and the monitor process. Preserve the files and logs before removal.
From a clean host, revoke and rotate GitHub, npm, PyPI, RubyGems, JFrog, CI/CD, cloud, and developer-tool credentials present in the affected build, workstation, process memory, or reachable environment.
Review GitHub audit logs for repository creation, workflow-file changes, deployment creation, commits named chore: update dependencies , and artifacts named reviewed .
Review npm, PyPI, RubyGems, and JFrog publish activity for unexpected versions and package tarball changes.
Inspect the persistence paths listed above, including LaunchAgents, systemd user units, .config/sysvinit-detect-fash , /var/tmp/.shit , temporary pcfg directories, and developer-tool configuration files.
Review SSH logs and known-hosts for outbound propagation attempts.
Do not trust package-manager cache state alone: the payload can persist independently through developer-tool configuration, GitHub token monitoring, CI workflow modification, and remote hosts.
Indicators of Compromise (IOCs) #
Malicious npm Package and Versions #
@7nohe/openapi-react-query-codegen
0.0.0-365d4eb738d3146583431948d3ba6e27a32556be
0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab
Threat Actor GitHub Infrastructure #
github[.]com/p00paboot
github[.]com/p00paboot/openapi-react-query-codegen — fork used to stage the malicious code
Malicious commit: 365d4eb738d3146583431948d3ba6e27a32556be
File and Persistence Indicators #
~/.local/bin/sysvinit-detect-fash.sh
~/.config/sysvinit-detect-fash/fox
~/.config/sysvinit-detect-fash/fash-detected
~/.config/sysvinit-detect-fash/runit
~/Library/LaunchAgents/com.user.sysvinit-detect-fash.plist
~/.config/systemd/user/sysvinit-detect-fash.service
b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6
59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380
d3246926b20a8d021ed7de0ac8e9eee1dda986088f84ba18f31cb2042a121f5d
Execution and Environment Indicators #
preinstall : node 3FWCvzduYZg.js
WORKFLOW_ID=release.yml
REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen
TARGET_PACKAGES=@7nohe/openapi-react-query-codegen
Observed Legitimate Infrastructure Abuse #
DO NOT BLOCK: Legitimate Service Endpoints
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
