groundy
security

Nx Supply-Chain Attack Used Developers' Own AI CLIs to Hunt Secrets

s1ngularity's malicious Nx packages invoked installed AI CLIs with permission bypass flags to enumerate secrets, making any local agent a scriptable recon primitive.

8 min · · · 4 sources ↓

The s1ngularity compromise of the Nx build toolchain in August 2025 did not stop at stealing npm credentials. Its postinstall payload detected whichever AI coding assistant was installed on the host, Claude Code, Gemini, or Amazon Q, armed each with a permission-bypass flag, and asked it to write a list of wallet files, SSH keys, and tokens to disk. An agent installed for developer productivity became a reconnaissance tool its owner never invoked.

What did s1ngularity compromise, and how was the npm token stolen?

s1ngularity was a supply-chain compromise of the Nx monorepo build system (published by Nrwl) in which attackers obtained Nx’s npm publishing token and pushed eight malicious package versions to npm for roughly five hours starting August 26, 2025. The root cause was a GitHub Actions injection. Nx’s pipeline used a pull_request_target workflow that echoed unsanitized pull-request titles into a command context, and that trigger granted an elevated GITHUB_TOKEN with read/write access to the repository. According to Wiz’s analysis, the attacker used that elevated token to trigger the publish.yml workflow, which publishes to npm using an npm token, and exfiltrated the credential in the process. (Wiz) The injection is the root cause; the AI-CLI reconnaissance is a payload layered on top of it.

The malicious versions spanned two ranges, nx 20.9.0 through 20.12.0 and nx 21.5.0 through 21.8.0, alongside the scoped packages @nx/devkit, @nx/js, @nx/workspace, @nx/node, @nx/eslint, @nx/key, and @nx/enterprise-cloud. They remained live about 5 hours 20 minutes before npm removed them. (Nx’s postmortem) The package sees over 3.5 million weekly downloads. (The Hacker News)

How did the payload invoke Claude, Gemini, and Amazon Q?

The malicious postinstall hook ran a file named telemetry.js, which on Linux and macOS hosts detected locally-installed AI coding CLIs and invoked each with that tool’s own permission-bypass flag. According to StepSecurity’s teardown, telemetry.js shelled out to three agents in turn: claude with --dangerously-skip-permissions, gemini with --yolo, and q (Amazon Q) with --trust-all-tools. (StepSecurity, Orca) Each was given a prompt instructing it to walk the filesystem and write the paths of matching secret and wallet files into /tmp/inventory.txt. The agent performed the enumeration; the malware pointed it at a target and collected the result.

CLIVendorBypass flag(s)What it suppresses
Claude CodeAnthropic--dangerously-skip-permissionsPer-command permission prompts
Gemini CLIGoogle--yoloTool-call approvals
qAmazon--trust-all-toolsTool-trust checks

This is the novel step, and it deserves separating from the token theft. Every other operation in the payload, harvesting ~/.npmrc, reading environment variables, or grepping for token prefixes, could be done with shell one-liners. Borrowing the AI CLIs bought the attacker nothing the shell could not already do, with one possible exception: process legitimacy. A process named claude or gemini touching files looks, at a glance, like a developer’s own workflow. Whether that was the intent or a proof-of-concept flourish is not something the sources establish.

What secrets did the payload exfiltrate, and through what channel?

telemetry.js harvested GitHub tokens, npm credentials, SSH private keys, environment variables, .env files, and cryptocurrency wallet keystores, then exfiltrated the lot through public GitHub repositories the attacker created with the stolen token. StepSecurity and Orca both name GitHub tokens, npm credentials, and SSH keys among the targets; Orca adds cryptocurrency wallets. (StepSecurity, Orca)

The exfiltration channel was brazen. The payload used the stolen GitHub token to create public repositories with names like s1ngularity-repository, committed triple-base64-encoded dumps, and left them world-readable. By the time GitHub disabled the attacker-controlled repositories around 9 AM UTC on August 27, thousands of secrets had already been exposed. (Orca, StepSecurity) Collapsing theft and exfiltration into a single credential meant there was no separate command-and-control channel to detect. The defender’s signal was a public repository full of base64, not a suspicious outbound connection.

Why are the permission-bypass flags the load-bearing exploit?

The three flags are the part that makes borrowed-agent reconnaissance work, because each tells its CLI to suppress the confirmation prompts that would otherwise stop a file-reading or shell command and ask for human approval. These flags exist for legitimate reasons: automated pipelines, batch tasks, and developers who don’t want to approve every read. A CLI running with one of them will execute whatever prompt it is handed, including a prompt handed to it by a postinstall script. The bypass is not a bug any of the three vendors shipped. It is a documented, intentionally-shipped affordance that an attacker repurposed.

The defense implication is narrow and worth stating plainly. A permission-bypassed local agent is a general-purpose shell with a model sitting in front of it. Running one of those flags on a machine that also holds production secrets, wallet keystores, or live deploy tokens is the equivalent of sourcing those credentials into an unattended shell. The agent’s guardrails are exactly the thing the flag removes.

What about persistence, sabotage, and the second wave?

Beyond reconnaissance, the payload sabotaged infected hosts, and the attackers staged a follow-on operation on August 28 that reached private repositories the original npm window never touched. For persistence and sabotage, telemetry.js appended sudo shutdown -h 0 to both ~/.bashrc and ~/.zshrc, so every new terminal session attempted an immediate system shutdown. (Wiz)

The August 28 second wave abused long-lived, leaked GitHub CLI OAuth credentials to rename private organization repositories to s1ngularity-repository-{random}, fork them into compromised user accounts, and flip them public. (StepSecurity) This extended the blast radius past the npm window into organizations whose only connection to the incident was a leaked gh token. A stolen long-lived credential outlives the incident that produced it, so rotation has to be sweeping rather than scoped to the obviously affected packages.

What did Nx harden, and what is the reusable checklist?

Nx’s remediation, documented in its postmortem, is a reusable publishing-hardening playbook for any open-source maintainer whose install volume would amplify a compromise: replace long-lived publish tokens with provenance-based trust, gate publishing behind CI and a human, and treat the workflow itself as the attack surface. The specific changes were to replace npm-token publishing with NPM Trusted Publishers via OIDC, enforce CI-only publishing with manual 2FA, disable pipeline runs for all external contributors (not only first-timers), add provenance verification in Nx and Nx Console, and publish a SECURITY.md. (Nx’s postmortem)

The general lessons reduce to three. Stop using long-lived write tokens in CI. Treat pull_request_target as a sharp edge: echoing untrusted PR content into a run that holds write permissions is the classic footgun. Publish provenance so consumers can verify what they install. Nx was a high-profile victim precisely because several million weekly downloads made it worth the effort, and the same playbook applies to any maintainer whose package propagates into CI runners.

Is every local AI CLI now a recon primitive?

The durable lesson from s1ngularity is not the specific indicators of compromise, which are historical artifacts, but the threat-model shift: an always-on local AI agent with shell access is a scriptable primitive an attacker can invoke without ever prompting it. Orca describes s1ngularity as “the first known supply chain attack to actively search for installed LLM tools on developer machines,” and StepSecurity calls it “the first known case where malware harnessed developer-facing AI CLI tools.” (Orca, StepSecurity) Both qualifiers should be read as “first documented,” not “first ever.” Orca and StepSecurity are security vendors selling detection, so the absence of prior cases in their records is evidence about their catalog, not about history.

A couple of caveats are worth flagging for anyone citing this incident. The AI Incident Database’s “Incident Date” field reads 2025-08-21, while its body states the packages went live August 26-27; the field and the narrative disagree. (the AI Incident Database) Orca’s claim that secrets could be discovered on GitHub within two minutes is vendor research, not a peer-reviewed measurement.

The architectural response is the part that travels. Scope each agent’s permissions to the minimum it needs for the task at hand. Sandbox its filesystem reach so it cannot read ~/.ssh, wallet directories, or credential files unrelated to the work. Never invoke a bypass flag on a secret-bearing host. Prefer short-lived, narrowly-scoped credentials so a stolen token expires before it can be exfiltrated and replayed. The borrowed-agent loop in s1ngularity worked because three CLIs were sitting on the box, each willing to run unsupervised against the filesystem. Removing any one of those three conditions, the bypass flag, the filesystem reach, or the credential’s longevity, breaks the loop.

Frequently Asked Questions

Did the AI-CLI reconnaissance fire on every infected host?

No. telemetry.js exited its AI-CLI branch early on win32, so the Claude, Gemini, and Q invocations only fired on Linux and macOS. The borrowed-agent recon surface is therefore confined to Unix developer boxes, where those CLIs are most likely to sit alongside the build toolchain; Windows hosts were still exposed to the rest of the payload.

What credentials and wallet types should incident response teams rotate beyond the npm token?

telemetry.js matched gh auth token output against the gho_ and ghp_ prefixes and read ~/.npmrc, id_rsa, .env files, and environment variables. Its wallet sweep targeted MetaMask keystores plus Electrum, Ledger, Trezor, Exodus, Phantom, and Solflare files. Any host that ran a malicious version needs all of these rotated, not just the publishing credentials.

How does the AI Incident Database classify s1ngularity?

It logs the incident as number 1210 under Risk Domain ‘Privacy & Security’ and subdomain 2.2, ‘AI system security vulnerabilities and attacks,’ tagging the entity as AI, timing as post-deployment, and intent as unintentional. That taxonomy pins the AI relevance on the CLI recon layer rather than the npm token theft. Its Incident Date field reads 2025-08-21, which conflicts with the August 26-27 package window its own narrative describes.

Why did the August 28 follow-on reach organizations that never installed the malicious packages?

It ran on long-lived leaked GitHub CLI OAuth credentials, which outlive the npm publishing token and carry broad repository scope by default. One such token could rename and publicize private repositories in organizations whose only link to the incident was an unrelated credential leak. Containment has to sweep every long-lived GitHub credential on the host, not just the Nx publishing secrets.

What indicators of compromise should defenders grep for on an affected host?

The durable signatures are the telemetry.js postinstall script, the /tmp/inventory.txt output file, public repositories named s1ngularity-repository with optional -0 or -1 suffixes, and triple-base64 dumps committed to a results.b64 file. Nx’s remediation checklist also instructs users to grep ~/.bashrc and ~/.zshrc for the literal string sudo shutdown -h 0.

sources · 4 cited