Incidents
Nx s1ngularity: malware that used the coding agents already installed
Nx · August 2025
Updated
In August 2025 attackers published malicious versions of the Nx build system to npm. The postinstall script did something the category had not seen before: rather than hunting for credentials itself, it invoked the Claude Code, Gemini CLI and Amazon Q CLI already installed on the developer's machine, with their permission prompts disabled by flag, and asked them to enumerate the filesystem for secrets. What they found was pushed to a public repository in the victim's own GitHub account. Researchers counted roughly 2,300 distinct secrets across more than a thousand accounts, and a follow-on wave used the stolen tokens to make over 6,700 private repositories public.
What happened
The entry point was not the agents. Attackers exploited an injection flaw in an Nx GitHub Actions workflow on 24 August 2025, took the npm publish token, and pushed eight malicious nx versions along with compromised @nx/devkit, @nx/js, @nx/workspace, @nx/node, @nx/eslint and @nx/enterprise-cloud releases. They were live for roughly four hours on 26 August. The postinstall payload scanned for the usual credential-bearing files - SSH keys, .env, .npmrc, wallet keystores - and then invoked whichever agent CLI it found with the flag that turns approvals off, prompting it to walk the home directory and write what it found to a file. Semgrep's analysis notes that Claude declined the instruction in their testing; the design did not depend on any one agent complying, which is what having three of them was for. The results were base64-encoded and pushed with the gh CLI to a newly created public repository in the victim's own account, and the script appended a shutdown command to the user's shell profile. Wiz put the first phase at over 1,700 users and roughly 2,300 secrets, including more than a thousand valid GitHub tokens, around 90 per cent of them still live; a second wave on 28 and 29 August used those tokens to flip 6,700-plus private repositories public across some 480 accounts.
The failure mode
Credentials were files on a disk that anything running as you could read, and the agent binaries already installed were available as both the search tool and the exfiltration path.
What Forkbench changes about it
- 1Keep the value out of the filesystem. A secret in Vault lives in the Keychain on your Mac rather than in a file, so a script walking $HOME for .env and .npmrc finds names it cannot use.
- 2Give a secret an established destination and it is not released to the command at all: the command gets an unguessable stand-in minted for that one terminal and that one secret, and a local proxy substitutes the real value into the outgoing request on its way to the host that key belongs to.
- 3Grant per Thread, so a compromised install in one job reaches the credentials of that job rather than of every job on the machine.
- 4Lock a Thread to the folders the work is about. Every shell in it starts under a kernel-enforced profile that is inherited by dependency install hooks, which is exactly the kind of process this payload ran in.
- 5Treat the flags as the tell. The options that skip permissions, trust all tools or run without interaction are what a permission prompt looks like when something other than you is answering it.
What Forkbench would not have stopped: Forkbench would not have stopped the package installing or the script running, and it does not police your filesystem. A postinstall runs as you, so anything your shell can read it can read: keys in ~/.aws, ~/.ssh or a project .env were exposed here and would be again. Vault covers what Vault holds. And a secret with no established destination cannot be substituted, so it is injected into the command's environment instead, where a program inside that process can still read what it was handed. This is containment and evidence, not isolation.
Frequently asked
Was this prompt injection?
No, and the distinction matters. Prompt injection is hostile text hidden in content an agent reads. Here the malware invoked the agent binaries directly, with its own prompts and their approvals disabled. It did not need to trick an agent; it only needed one to be installed.
Did the agents comply?
Semgrep reported that Claude declined the instruction in their testing. The payload targeted three different CLIs, so compliance by any one of them was enough, and treating a refusal as the control is the wrong lesson to draw.
Does Vault stop a malicious postinstall?
No. It runs as you and Forkbench does not stand between your shell and your disk. What changes is what such a script finds: a value in the Keychain, released only to a command and only towards the host it belongs to, is not a string sitting in a file waiting to be copied.
Related: Getting the value out of the file in the first place, What to rotate, and in what order
Sources
- Nx: the s1ngularity attack postmortem
- GitHub security advisory GHSA-cxm3-wv7p-598c (CVE-2025-10894), nrwl/nx
- Wiz: the s1ngularity supply-chain attack, scale and second wave
- Semgrep: Nx compromised to steal wallets and credentials, with the agent prompts and flags
- StepSecurity: supply-chain security alert on the compromised Nx packages