Incident · npm package compromise

The Nx npm compromise: what it did on a developer’s Mac, and what Kilobat would have shown

For about five hours on 26 and 27 August 2025, eight versions of the Nx build system on npm carried a postinstall script that searched developers’ machines for secrets, enlisted the AI coding CLIs it found to help, and published what it collected to a new public repository in the victim’s own GitHub account.

When: 26–27 August 2025Sources checked: 1 November 2026

In short

  • Protection: Wouldn’t help

    Nothing to block. The code came from registry.npmjs.org and the data went to api.github.com, which no threat list names.

  • Agent guard: Wouldn’t help

    No paste site, tunnel or raw IP address was involved, and the upload cut leaves well-known hosts such as api.github.com alone, by design.

  • Agent network log: Limited

    It would have recorded an install script talking to the GitHub API, and the AI CLI runs the script started, but only for installs on this Mac and not as a block.

What happened

The attacker got Nx’s npm publishing token through the project’s own CI. According to the maintainers’ advisory, a GitHub Actions workflow merged on 21 August used the pull_request_target trigger and echoed pull request titles into a shell, so a crafted title could run commands with the repository’s permissions. On 24 August that path was used to start the publishing workflow from a malicious commit and send the npm token to a webhook.

With the token, the attacker published nx 20.9.0–20.12.0 and 21.5.0–21.8.0, plus versions of @nx/devkit, @nx/js, @nx/workspace, @nx/node, @nx/eslint, @nx/key and @nx/enterprise-cloud. Each carried a postinstall hook that ran telemetry.js, which only acted on macOS and Linux.

The script looked for the claude, gemini and q command-line tools and, where they were installed, ran them with the flags that skip their permission prompts (--dangerously-skip-permissions, --yolo, --trust-all-tools) and a prompt asking them to list files that looked like wallets, keys and .env files into /tmp/inventory.txt. Snyk and StepSecurity published the code. Wiz saw this succeed in hundreds of cases, with the AI providers’ guardrails refusing some of the time.

It then read those files, collected the GitHub CLI’s token with gh auth token and the npm token from ~/.npmrc, created a public repository named s1ngularity-repository in the victim’s account and uploaded everything as triple-base64 results.b64. It also appended sudo shutdown -h 0 to ~/.zshrc and ~/.bashrc, so every new terminal asked for a password and shut the machine down.

Many installs weren’t typed by anyone. The maintainers found that the Nx Console editor extension, versions 18.6.30 to 18.65.1, checked for the latest nx on startup, so opening an editor during the window was enough. Wiz also found the malware running in CI pipelines. Wiz counted over a thousand valid GitHub tokens among the leaked data; in a second phase, stolen tokens were used to make more than 5,500 private repositories public.

Timeline

  1. 21 Aug 2025

    A workflow with a shell-injection flaw is merged into the Nx repository (maintainers’ advisory).

  2. 24 Aug 2025

    A crafted pull request starts the publish workflow from a malicious commit, which sends the npm token to a webhook.

  3. 26 Aug, 22:32 UTC

    nx 21.5.0 and several @nx/* packages are published with telemetry.js; 20.9.0 follows seven minutes later.

  4. 27 Aug, 00:30 UTC

    A user opens a GitHub issue about the malicious versions. The last two, 21.8.0 and 20.12.0, appear at 00:37.

  5. 27 Aug, 02:44 UTC

    npm removes the affected versions and the publish tokens.

  6. 27 Aug, about 09:00 UTC

    GitHub disables the attacker-created repositories, roughly eight hours after the first ones appeared (Wiz).

  7. 27 Aug 2025

    Nx Console 18.66.0 stops installing the latest nx; Nx moves publishing to npm’s Trusted Publishers with 2FA.

  8. 28–29 Aug 2025

    Stolen tokens are used to rename private repositories and make them public; Wiz counts over 400 accounts and 5,500 repositories.

What it looked like on the network

On a Mac, the attack used two hosts, both ones a developer’s machine talks to every day. The package came from registry.npmjs.org like any other. The theft went to api.github.com, as the victim, using the victim’s own GitHub CLI token: a POST /user/repos to create the repository, then a PUT /repos/<owner>/s1ngularity-repository/contents/results.b64 with the encoded data. There was no command-and-control server and no other upload destination.

The process tree was short. npm (or the editor extension) ran a shell, the shell ran node telemetry.js, and node started claude, gemini or q as child processes. Each AI CLI talked to its own provider, as it does in normal use; the reading of files happened locally. The upload itself came from node. Its size depended entirely on what the files held.

Indicators

As the sources document them. Domains are text, never links, and are defanged ([.]) where the source defangs them. Most of this infrastructure is long gone: indicators help you check old logs, files and accounts, not block today’s campaigns.

IndicatorTypeWhere it showsNote
api.github.comHost (legitimate)Your MacPOST /user/repos, then PUT …/contents/results.b64, made by node during an npm install.
s1ngularity-repository, s1ngularity-repository-0, s1ngularity-repository-1GitHub repository nameGitHub accountPublic when created; GitHub disabled them on 27 August. Renamed repositories in the second phase used s1ngularity-repository- plus five letters.
results.b64FileGitHub repositoryThe collected data, base64-encoded two or three times.
/tmp/inventory.txt, /tmp/inventory.txt.bakFileYour MacThe AI CLIs’ list of file paths.
sudo shutdown -h 0Line in ~/.zshrc, ~/.bashrcYour MacShuts the Mac down when a terminal opens and the password is given.
nx 20.9.0–20.12.0, 21.5.0–21.8.0npm versionsLockfiles, npm cacheAlso @nx/devkit, @nx/js, @nx/workspace, @nx/node 20.9.0 and 21.5.0, @nx/eslint 21.5.0, @nx/key and @nx/enterprise-cloud 3.2.0.

What Kilobat would have shown or blocked

  • Agent network log. When an agent ran the install, say Claude Code running npm install, its session would list node under npm sending to api.github.com in the middle of an install: an install script talking to the GitHub API is worth a look. It would not be flagged first time, because api.github.com is a well-known host, and it would be flagged large upload only above 10 MB.
  • Agent network log. Kilobat recognises Claude Code and Gemini CLI by their executables, not by who started them, so the runs the script started would appear as their own sessions, in the project folder, at a moment you hadn’t started an agent. Amazon’s q isn’t one of the agents Kilobat recognises.
  • Protection. Nothing. registry.npmjs.org and api.github.com are not threats, and Kilobat should never block them.
  • Agent guard. Nothing either. The guard’s rules cover paste sites, file shares, tunnels, request catchers and raw IP addresses, none of which appeared, and its upload cut skips well-known hosts so that git push keeps working.

Where Kilobat wouldn’t have helped

  • Exfiltration to github.com with the victim’s own token looks the same as the GitHub CLI at work. Kilobat sees hosts and byte counts, never what was sent.
  • The npm token was stolen in Nx’s GitHub Actions, on GitHub’s runners, and the second phase ran from the attacker’s side with stolen tokens. Neither touched your Mac.
  • Installs started by an editor extension in VS Code aren’t attributed to an agent, so they wouldn’t get an agent session; in Cursor and Windsurf they count as the app’s.
  • Reading files, running the AI CLIs’ file search and editing ~/.zshrc aren’t network events.
  • The agent network log keeps 30 days, so it can’t tell you now whether this ran on your Mac in August 2025.

Check your Mac now

  1. In each project, see whether an affected version of Nx is installed. Update if it is.

    npm ls nx
  2. Look for the files the script left. macOS clears /tmp on restart, so finding nothing proves little.

    ls -la /tmp/inventory.txt /tmp/inventory.txt.bak
  3. Check your shell startup files for the shutdown line, and delete it if it’s there.

    grep -n "shutdown" ~/.zshrc ~/.bashrc
  4. In GitHub’s security log, look for a repo.create event with s1ngularity-repository in the name: github.com/settings/security-log. The repository itself may already be disabled.

  5. If you were affected, revoke the GitHub CLI’s access under Settings › Applications › Authorized OAuth Apps, sign in again with gh auth login, and rotate npm tokens and every secret in your .env files and environment.

  6. Clear the npm cache so an old copy can’t be installed again.

    npm cache clean --force
  7. For the next one: with Kilobat’s agent network log recording, look at what each install an agent ran contacted. Watch for npm or node talking to api.github.com, and for agent sessions you didn’t start.

    kilobat agent-log --all

Questions

Did the AI tools turn on their users?

No. The malicious script started them itself, with the flags that switch off their permission prompts, and gave them its own instructions. The same flags are a reason to be careful about running agents unattended with full permissions.

Why doesn’t Kilobat block large uploads to GitHub?

Because pushing code, releases and packages to GitHub is ordinary work, and cutting it off would break git push. The log shows the upload with its bytes and process instead, so you can see it.

Sources

Checked on 1 November 2026.

See what your agents and apps connect to

Kilobat is a Mac menu-bar app that names the app, process and agent behind every connection, keeps them away from known-bad hosts, and logs what each agent session reached.

Download for Mac ↓
brew install seifip/kilobat/kilobat