Every npm install is a stranger you agreed to run as root. (Comforting, isn't it?) That is the shared trust flaw the fourth entry in Docker's Coding Agent Horror Stories, published on July 28, walks its readers through: a widely-installed Nx package briefly turned into a secret-exfiltration weapon, and every CI runner that pulled it did the attackers' work for them.
The post, by Ajeet Singh Raina, is Part 4 of the series. It revisits the August 26, 2025 compromise of the Nx build package on npm. Nx pulls roughly four million downloads a week. The malicious releases carried a post-install hook pointing at a file called telemetry.js, and that file ran on every machine that pulled the package. Developer laptops. Build agents. CI runners. Anywhere a lifecycle script could fire, telemetry.js executed with whatever credentials the environment had lying around: .env files, SSH keys, cloud configs, npm and GitHub tokens. The campaign picked up the name s1ngularity from the public repositories it created to hold what it stole.
The bit CI teams keep pretending isn't a problem
Blaming Nx is a category error. Any popular package that lets a maintainer publish arbitrary code will one day publish arbitrary code by the wrong person. The question a CI/CD team should actually take home is quieter: why does an install step ever have access to the runner's secrets?
You know the answer. Because it always has. Pipelines have been written for a decade on the polite assumption that the code you run under npm install is the code you would have run anyway. The post-install hook is the softest trust boundary in the toolchain. It is authored by dependencies you rarely audit, it fires before anyone gets to run a test, and it inherits the full environment of the calling shell. Signed. Verified. Still exploitable.
What "isolate the install" actually asks of you
Docker's argument, unsurprisingly for Docker, is that the answer is a sandbox: the install step should not see the runner's secrets in the first place, and the containment layer should be where you enforce that. Take the vendor pitch with the amount of salt it deserves. The underlying move, though, is one every CI/CD platform is going to have to make, and it does not depend on any one tool.
- Do not put long-lived secrets in the environment your dependency install can read. Fetch credentials after the install step, in a separate stage, ideally via a short-lived OIDC token instead of a static one.
- Turn off lifecycle scripts where the ecosystem lets you (
npm ci --ignore-scriptsand its peers) and enable them only for the vetted packages that genuinely need them. - Pin dependencies by content, not by tag. A version range is a permission slip you signed for whichever release lands in the window between commit and build.
- Split the runner. The job that resolves dependencies should not be the job that reads the deployment key.
None of that is new. None of that is fun to retrofit. All of it would have caught s1ngularity reading ~/.aws/credentials out of a hot runner.
What the changelog leaves off
The horror-stories series has become useful because it names the pattern plainly: an agent, or a dependency, or an automated bot, running with the developer's full credential set, in a runtime with no isolation between "the thing I typed" and "the thing that got installed." What the Docker post skips over is that install-time isolation is not a solved problem for every stack. Package managers that let scripts write outside the workspace, monorepos that share credentials across every project, and CI runners that mount the whole team's secrets into every job are all live footguns.
The specific breach here is a year old. The class of breach is going to age like milk.
Verdict
If you run npm packages in CI, and you do, assume every install is a shell you invited. Not eventually. Now. The next telemetry.js is already in a maintainer's clipboard, waiting for a token rotation someone forgot to schedule.
Top comments (0)