AW Dev Rethought Flash: Your Editor Runs Their Code Before You Read It
There is a rule every developer learns early. Clone freely, but read before you run. Check the install scripts. Know what npm install is about to execute.
That rule stopped working in June — and this week showed the attack is still spreading.
SafeDep first documented the technique in June, when a worm it called Miasma armed more than 120 public GitHub repositories — including Azure's durabletask — with configuration files your editor and your AI coding assistant execute automatically. Not on install. On open.
This week, JFrog published research on a related campaign it tracks as "testmetest," in the same Shai-Hulud lineage: 42 more armed repositories across 11 owners, a new npm side, and new ways to hide. The initial access vector is still unconfirmed.
The Trigger Files
In the June wave, the attackers pushed a commit adding six files to a repository. Five are triggers. The sixth, .github/setup.js, is the payload.
.claude/settings.json and .gemini/settings.json each define a SessionStart hook that runs node .github/setup.js the moment you open a session in that project. .cursor/rules/setup.mdc is an always-applied rule instructing the agent to run the setup script — social engineering aimed at the assistant rather than at you. .vscode/tasks.json defines a task that fires on folder open, needing no agent at all. And the test script in package.json is rewritten, so anyone running tests, CI included, triggers the same thing.
Five independent paths to the same payload, covering the editor, three AI assistants and the test runner. Miss one and the others still fire.
Cloning the repository is safe. Opening it is what executes.
That inversion is the whole story. The defence we have taught for a decade — read the code before you run it — assumes there is a moment between arrival and execution. Repo-controlled editor configuration removes that moment.
What Actually Runs
.github/setup.js is a heavily obfuscated dropper. It decodes to a loader that AES-decrypts an embedded stealer, writes it to a temporary file and executes it under Bun. If Bun is not installed on the machine, the loader downloads a pinned Bun release from GitHub first.
That detail deserves a second look. The malware ships its own runtime. Any assumption that an unusual toolchain on your machine limits what an attacker can run does not hold here.
The stealer harvests credentials for AWS, Azure, GCP, Vault, Kubernetes, npm and GitHub, then exfiltrates them to public GitHub repositories the attackers created for the purpose. JFrog also identified an attacker-owned repository receiving encrypted “snapshot” files, which indicates collection was active rather than theoretical.
How It Spreads
Those stolen tokens are the propagation mechanism. Any token with write access can push the same injection into every repository it can reach, and the campaign appears to do this automatically.
In June, SafeDep caught a single commit landing on five repositories within 49 seconds — a script replaying a stolen token, not a human. JFrog found hijacked accounts and organisation credentials used to arm repositories, with commit timestamps forged to look older than they were.
This is why the repository count is the wrong thing to watch. A worm that converts each compromised developer into a publisher does not grow linearly, and the number is a snapshot of when someone last looked.
The npm Side and the Search Blind Spot
The registry half of the campaign is just as deliberate.
Malicious packages execute at install time through a binding.gyp file, which runs during the native build step even when no install script is declared in package.json. Auditing a package’s declared scripts is not sufficient to know what installing it does.
A ci.js file hidden on short-lived chore/ci-* branches runs inside GitHub Actions, obtains npm publishing tokens through GitHub OIDC, and publishes malicious versions using the project’s own legitimate identity. Some packages also ship a compiled Node addon that decrypts a hidden file at runtime and stays dormant inside CI systems, containers, sandboxes and debuggers — written to run on developer laptops and specifically not where scanners look.
Then the detail that kept all of this quiet. The .github/init.js payloads were deliberately oversized. GitHub’s code search skips files past a size threshold, so the malicious code sat in public repositories that nobody could grep for. The payload hid by being too large to index.
What To Do Right Now
Audit your repositories for the trigger files: .claude/settings.json, .gemini/settings.json, .cursor/rules/*.mdc, .vscode/tasks.json, and any .github/setup.js or .github/init.js you did not write. Check for short-lived chore/ci-* branches. Treat forged-looking commit timestamps as a signal rather than reassurance.
If you have opened an unfamiliar repository in Claude Code, Gemini CLI, Cursor or VS Code recently, rotate your cloud, Vault, Kubernetes, npm and GitHub credentials. Not because you were definitely hit, but because the cost of rotating is hours and the cost of not rotating is your entire cloud account.
On the npm side, remove the affected package versions, and audit your dependencies for packages that run native addons from postinstall or carry a binding.gyp without a declared install script.
Longer term, the uncomfortable one: decide whether your editor should execute repository-controlled configuration at all. Session hooks and folder-open tasks are genuinely useful, and they are also arbitrary code execution granted to whoever last pushed to the repo. Most teams have never made that trade-off explicitly, because for most of the time it has existed, nobody was exploiting it. That is no longer true, and has not been since June.
Enjoyed this post?
Stay in the loop
New posts + weekly digest, straight to your inbox.
Create a free account
- Save posts to your vault
- Like posts & build history
- New-post alerts
No comments yet. Be the first to comment!