Armando found that @squawk/fixes v0.3.6, an npm library for aviation fix and waypoint queries, was published on 2026-05-11 with a preinstall hook that fetches the Bun runtime and executes a 530 KB obfuscated worm bundle named router_init.js. The package has 4,125 total downloads and 2 direct importers. Spectra Intelligence had not marked the package as a whole malicious at the time of analysis, although the shipped files carry malicious detections.
Key Findings
Malicious version: v0.3.6, published 2026-05-11 (23:51 UTC), the fifth of five consecutive versions (v0.3.2 to v0.3.6) published the same evening; the previous release, v0.3.1 (2026-05-06), was not flagged.
Entry point: the preinstall lifecycle hook runs node setup.mjs automatically on npm install.
Targets: Linux (glibc and musl), macOS and Windows on x64 and arm64; the loader picks a matching Bun build and runs the worm bundle with it.
Evasion: the loader exits early if bun is already on the PATH; the payload is a 530 KB single-line bundle with string-array obfuscation.
Exfiltration: HTTPS to git-tanstack.com; fallback to a public repository created in the victim's GitHub account with the description "Shai-Hulud: Here We Go Again"
Destructive fallback: can install a gh-token-monitor service that deletes the user's home directory (rm -rf ~/) if the stolen GitHub token is revoked
@squawk/fixes provides fix and waypoint queries by identifier, location or name search. It is part of the @squawk family of TypeScript aviation libraries built from the public neilcochran/squawk repository, was first published on 2026-04-12, depends on @squawk/geo and @squawk/types, and has 4,125 total downloads and 2 direct importers. Nothing in that purpose requires running code at install time.
Attack Analysis
Compared with the library's normal contents, v0.3.6 ships two extra files at the package root, setup.mjs and router_init.js, and the scripts block of package.json consists of a single preinstall entry. Neither file is covered by the manifest's files allow-list (dist), so they were not produced by the package's normal packaging configuration.
When npm installs the package, setup.mjs runs first. It checks whether bun already responds on the PATH; if so it returns silently and the payload is not launched. Otherwise it maps the platform and architecture (including a musl check for Alpine) to a Bun release asset, downloads version 1.3.13 of Bun from the project's GitHub releases page into a temporary bun-dl- directory, extracts it with unzip, PowerShell Expand-Archive, or a built-in ZIP reader, marks it executable, and runs router_init.js with it. The temporary directory is deleted afterwards. Using a freshly downloaded runtime means the payload does not depend on the victim's Node version.
router_init.js is a 530 KB single-line bundle hidden behind an obfuscated string array. Plaintext strings recovered from it show references to AWS access key variables, the cloud instance metadata credential endpoint, GitHub Actions OIDC token request variables, the npm OIDC token exchange endpoint, the Sigstore Rekor transparency log, GitHub API endpoints, HashiCorp Vault references, Kubernetes service account paths, and .claude and .vscode configuration paths. Together with the Bun-based loader, this is the structure of a credential-harvesting, self-propagating npm worm that abuses CI identity and package provenance services.
The publisher, the npm account neilcochran, maps to a GitHub user created in December 2019 with a public history of unrelated TypeScript projects, so this is an established account rather than a throwaway. The account publishes 30 npm packages, and many of them (including most of the @squawk scope, cross-stitch and wot-api) received new releases within the same evening of 2026-05-11. The pattern is consistent with a compromise of the maintainer's publishing credentials or pipeline being used to republish many packages with the same injected kit; the data does not show how access was obtained. Later releases (v0.3.7 and newer) were not flagged at the time of analysis.
Armando deobfuscated router_init.js statically, without executing it, through three layers: the string array, a per-byte string cipher keyed with PBKDF2-SHA256, and AES-256-GCM encrypted, gzip-compressed embedded files. On a developer machine the bundle relaunches itself as a detached background process so it keeps running after npm install returns, takes a single-instance lock file named tmp.ts018051808.lock in the temporary directory, and exits without doing anything if the system locale is Russian. It collects credential files, the output of gh auth token, AWS credentials (including SSM parameters and Secrets Manager secrets across regions, found through environment variables or the instance and container metadata endpoints), Kubernetes service account tokens and secrets, HashiCorp Vault tokens, and pattern matches for cloud, payment, SSH and Docker secrets. On Linux GitHub Actions runners it uses sudo to read the memory of the Runner.Worker process and search it for secrets.
Collected data is sent over HTTPS to git-tanstack.com (path /router), an attacker-controlled domain named to resemble the TanStack project. This build has no Session messenger channel. If the primary channel is unavailable, the bundle creates a public repository in the victim's own GitHub account with the description "Shai-Hulud: Here We Go Again" and commits the data there under the name github-advanced-security[bot].
The bundle spreads through every credential it finds. With a GitHub token that has workflow scope, it pushes a fake "Add CodeQL Analysis" workflow, committed as github-advanced-security[bot], that writes all repository secrets to a build artifact named format-results, and then downloads that artifact. With a GitHub App token it pushes a copy of itself as .claude/router_runtime.js together with .claude/settings.json, .claude/setup.mjs, .vscode/tasks.json and .vscode/setup.mjs in a commit titled "chore: update dependencies" by the author claude. The .claude SessionStart hook and the .vscode folder-open task relaunch the loader whenever a developer opens the repository in Claude Code or VS Code. With an npm token it republishes the victim's packages with the loader added as a preinstall hook and the patch version bumped. Inside the tanstack/router release workflow it instead adds an @tanstack/setup optional dependency to the published tarball.
The bundle also carries a destructive fallback. When it has to use the GitHub fallback channel with a stolen classic or OAuth token that has repo scope, on an account that belongs to no organizations, it installs a gh-token-monitor service: a systemd user unit gh-token-monitor.service on Linux or a LaunchAgent com.user.gh-token-monitor on macOS, running ~/.local/bin/gh-token-monitor.sh. For 24 hours the service checks the token against the GitHub API every 60 seconds. If the token is rejected, which is what happens when the victim revokes it, the service runs rm -rf ~/ and deletes the user's home directory.
Malicious Files
setup.mjs is the install-time loader. It exits if Bun is present, otherwise downloads Bun 1.3.13 for the host platform from GitHub releases and executes router_init.js with it. It has no relation to the library's stated purpose.
router_init.js is the 530 KB single-line obfuscated worm bundle. Its strings reference AWS access keys, the instance metadata credential endpoint, GitHub Actions OIDC request variables, the npm OIDC token exchange, Sigstore Rekor, GitHub APIs, Vault and Kubernetes service account paths.
package.json adds the preinstall hook that launches setup.mjs.
Recommendations
Developers and security teams should take the following steps:
Identify exposure: Check whether @squawk/fixes v0.3.6 (or v0.3.2, v0.3.3, v0.3.4, v0.3.5) appears in your dependency tree, including transitive dependencies. Any machine or pipeline that ran npm install while these versions were current should be considered potentially affected.
Update immediately: Move to a release outside the 2026-05-11 window, such as v0.3.7 (2026-06-14) or later, after verifying it yourself, and pin it in your lockfile.
Contain before revoking tokens: On any affected Linux or macOS host, first stop and remove the gh-token-monitor service (systemd user unit gh-token-monitor.service or LaunchAgent com.user.gh-token-monitor, plus ~/.local/bin/gh-token-monitor.sh and ~/.config/gh-token-monitor/), or isolate the host from the network. Revoking a stolen GitHub token while that service runs triggers deletion of the home directory. Then rotate npm tokens, GitHub tokens, cloud, Vault and Kubernetes credentials and CI secrets reachable from those hosts, and review packages published from those identities.
Inspect affected systems: Look for a bun-dl-* temporary directory, an unexpected Bun download, and unexpected npm publishes or GitHub activity. Also look for a tmp.ts018051808.lock file in the temporary directory, the gh-token-monitor files above, .claude/router_runtime.js, .claude/setup.mjs, .claude/settings.json, .vscode/tasks.json and .vscode/setup.mjs in your repositories, new public repositories in your GitHub account described as "Shai-Hulud: Here We Go Again", commits titled "Add CodeQL Analysis" by github-advanced-security[bot] or "chore: update dependencies" by claude, and workflow artifacts named format-results.
Scan your software supply chain: Use Spectra Assure Community to check whether packages in your environment have been flagged.