Your Agent Reads the Hooks: testmetest Armed 42 GitHub Repos to Execute Through Claude, Gemini, Cursor, and VS Code

On 26 October 2026, JFrog Security Research published the finding that turns the testmetest npm campaign from a package problem into a repository problem: 42 armed third-party GitHub repositories across 11 legitimate owners, each carrying execution hooks for AI coding assistants and IDEs plus a polymorphic wrapper of 12.9 to 13.7 MB that decodes to the same hidden publisher payload. The size is the camouflage. GitHub code search never indexed the blobs and the contents API cannot return files that large, so the first sweep found only 26 repositories through their searchable hook files — full enumeration through the Git trees and blobs APIs raised the total to 42, every reachable victim repository armed, with no population of empty placeholders. SafeDep had documented the npm side of the campaign on 22 October and explicitly left the lineage question open; JFrog confirms shared tooling and a Shai-Hulud-descendant payload but states plainly that the evidence does not establish that the original Miasma operators are responsible. We read the full JFrog report first-hand.

Readers of this site have seen the Shai-Hulud shape before — the TensorLake 0.5.144 worm that kept valid provenance, the hijacked coding assistant that reached 100 internal repositories, and the retrospective on AI agents living off the land. What is new here is the delivery engineering around the payload: hooks aimed at the assistant rather than the human, a native-addon disguise, a publisher that mints its own npm tokens from GitHub identity, and a timeline the operator forged. This briefing does not relitigate those earlier worms; it covers what JFrog proved about this one.

The npm chain hides behind a build file

Malicious testmetest releases carry no conventional install script. Instead, binding.gyp uses a command expansion to execute index.js during installation: a target with "sources": ["<!(env node index.js >/dev/null 2>&1; echo stub.c)"]. The loader applies a per-build Caesar transformation to a character-code array, decrypts an AES-128-GCM stage, and locates or downloads Bun 1.2.18. Depending on the build, the decrypted payload is piped to bun run - or written to a temporary .b-* directory and removed after execution. The full npm payload steals credentials, poisons repositories and CI configuration, and propagates through registries. One release, event-bus-lite-testmetest@0.0.2, is an unusual dual-stage carrier holding both the credential-stealing worm and a separate CI-publisher payload. As of 26 October the registry inventory spans 25 package names and 82 version records, including benign stubs and decoys — JFrog warns explicitly that listing a package in the inventory does not make its benign releases malicious.

The payload moved into a native addon

Later in the campaign the operator stopped shipping a JavaScript loader at all. Three packages — @iobandamwer/cache-helper, @iobandamwer/native-helper, and path-stream-utils-testmetest — carry a precompiled Node addon (addon.node, plus addon_win.node for Windows) and a one-line postinstall that calls it, next to a dotfile named .napi_cache that looks like leftover build cache. At runtime the addon finds its own directory via /proc/self/maps, XORs every byte of .napi_cache with 0x96, drops the resulting JavaScript to /tmp, locks it down, and runs it with node in a detached process. The same encrypted file serves both platforms; only the drop path differs (/tmp/.<pid> on Linux, %TEMP%\.nc<PID>.js hidden on Windows), and JFrog found no code that deletes the dropped file afterward — a useful host artifact for responders.

Before touching the cache, the addon runs a long gating checklist and returns silently if anything trips: CI environment variables, container markers in /proc/1/cgroup, machines with roughly 1.5 GB of RAM or less or a single CPU, debuggers via TracerPid — and on Windows, a process-list walk for Wireshark, Fiddler, Process Monitor, Process Explorer, x64dbg, OllyDbg, and HTTP Debugger. The practical effect, as JFrog puts it, is that the payload fires on a real developer laptop or server and stays asleep inside the CI runners, containers, and sandboxes that automated package scanners use. This route is aimed at people; the ci.js publisher below was built to run inside GitHub Actions instead.

The hidden publisher mints its own npm tokens

The attacker-owned CementBeetleSteeple account hosted 17 campaign repositories. Several looked like normal source repositories on their default branches, but short-lived branches named chore/ci-<hex> carried a root-level ci.js and an Actions workflow — 68 pushed branch heads recovered, 16 distinct polymorphic wrapper builds. The workflow binds the payload to a target with a three-variable contract (TARGET_PACKAGES, WORKFLOW_ID, REPO_ID_SUFFIX), and the decoded second stage, roughly 2.2 MB, checks those values against the Actions context before doing anything. Once the guard passes, the publisher harvests GitHub tokens, hunts files for token-shaped values, configures Git credentials, inspects workflows for trusted-publishing prerequisites — and then performs a GitHub OIDC-to-npm token exchange, requesting an Actions identity token scoped to npm and trading it for a publishing token to ship a rebuilt, postinstall-injected package. Clean source repositories could therefore produce malicious registry artifacts without retaining anything malicious on their default branches. JFrog adds an important restraint: the recovered code establishes the intended automated mechanism, and the one worm release with a firm timestamp (data-pipeline-testmetest@2.0.1, 21 October 11:13:23 UTC) has no retained Actions run aligned with it — likely published manually or from a deleted run.

Two details show the operator rehearsing the whole loop. Some publisher branches carried a workflow named CodeQL that performed no scanning at all: it base64-dumped the environment, including NPM_TOKEN, into a one-day artifact. JFrog captured 29 of these before expiry and found three distinct real-format npm access tokens rotated across six repositories — handled as compromised secrets and not reproduced. And the attacker-owned stable-scripts-9965 repository, which began as a README-only reference, became a live encrypted data sink: 27 data/snap-*.json files totaling about 385 KB committed between 22 and 24 October under the message chore: sync. JFrog inspected one file transiently to confirm the schema — three redacted string fields consistent with an encrypted envelope — and explicitly declines to infer victim identities or plaintext contents from metadata alone.

Forty-two repositories, one payload, forged history

Across the 42 victim repositories JFrog found 10 distinct wrapper blobs. Decoding proved all 10 contain the same 4,067,677-character stage-one loader and decrypt to a byte-identical 2 MB payload (SHA-256 9d83919b…). The size differences are an encoding artifact of the ROT key choice, not different capabilities. In victim repositories the payload launches detached with ignored output so the hooks never block — including a Claude SessionStart hook running node .github/init.js || bun .github/init.js, with equivalent triggers planted for Gemini, Cursor MCP configuration, and VS Code folder-open tasks, plus package.json, composer.json, or Makefile modifications in some projects. Routine developer actions, including opening a workspace with a supported tool, become execution opportunities.

The commit history initially suggests months of activity; JFrog separates genuine July smoke testing from a scripted arming batch on 28 September that copied old messages and author dates, appended skip-checks:true, and pushed many repositories within seconds. The human cost is visible in the hijacked identities: the leamasuero account, an Argentine PHP developer with genuine 2014–2022 history, renamed repositories en masse with a come-to-telegram-rickluminari1-- prefix while 13 were armed in a two-minute batch; the legitimate Crayon-Biz-LLP organization had four repositories armed with commits under at least two member identities; and cross-account commits under a hijacked hakimrasdi identity tie the armed ismahhaziq/Wedding repository to stolen collaborator access. Shellpanel artifacts in the same hijacked-owner cluster link the GitHub victims to previously reported GitHub Actions exploitation of internet-facing cPanel systems — operational linkage, JFrog stresses, not one proven continuous execution chain.

What to do

  • Enumerate repository trees directly; do not trust code search. The campaign proved that 13 MB blobs fall below every content path defenders normally use. For each owner, resolve every public repository default-branch tree and check the .github subtree, fetching init.js candidates as Git blobs by digest.
  • Hunt the hook files and the branch names. Look for .claude/settings.json, .gemini/settings.json, .cursor/mcp.json, .cursor/rules/setup.mdc, .vscode/tasks.json, .github/init.js, addon.node / .napi_cache pairs, and short-lived chore/ci-* branches — plus the net-health-checker and pkg-runtime persistence names and deps/upd-* branches documented in the campaign.
  • Sweep temp directories for the dropped decrypt. /tmp/.<pid> on Linux and hidden %TEMP%\.nc<PID>.js on Windows may persist after execution. Treat any hit as a compromised host, not a cleaned one: the worm steals SSH keys, registry tokens, cloud, Kubernetes, and AI-tool credentials.
  • Rotate npm and GitHub tokens and audit trusted publishing. The publisher explicitly inspects workflows for id-token: write and trades OIDC identity for npm tokens; the fake-CodeQL artifacts show the operator testing credential collection end to end. Review Actions artifacts with short retentions for environment dumps before rotating.
  • Put a cooldown on agent hook execution. The Cursor-marketplace trojanized Claude Code build we covered on 31 October and the GhostAction wave across 772 repositories share the lesson: restrict editors to approved publishers, pin hook sources, and treat the update channel as a permission — the Slugfest trusted-publishing confusion shows the registry side of the same trust problem.

Our verification was read-only: we read the complete JFrog Security Research report of 26 October 2026 first-hand, including the binding.gyp loader construction, the native-addon gating and decryption logic, the package inventory, the ci.js OIDC-exchange excerpts, the fake-CodeQL workflow, the 42-repository enumeration methodology, and the hijack case studies. Register, heise, Ostorlab, and SafeDep coverage is carried by URL only. We cloned nothing, ran no package, and contacted no vendor.

Sources: