The npm Worm Era and the Defender's Playbook

A massive sandworm-like shape rising from a desert landscape, lit by blue points of light.

Between August 2025 and May 2026, npm shifted from occasionally hosting malware to becoming one of the most actively exploited software supply chains on Earth. Self-replicating worms (Shai-Hulud, Shai-Hulud 2.0, Mini Shai-Hulud, SANDWORM_MODE), a maintainer-phishing wave that briefly poisoned 2.6 billion weekly downloads of chalk and debug, and the first documented abuse of locally authenticated AI assistants collectively forced a new defensive posture.

ReversingLabs measured a 73% year-over-year increase in malicious open-source packages in 2025, with npm accounting for roughly 90% of all open-source malware. Sonatype tracked 512,847 malicious packages in a single year, a 156% jump. AI is now a tool for attackers (worm recon, phishing lures, just-in-time payload generation), a target (LLM API keys, MCP servers, Ollama and LM Studio endpoints), and one of the few asymmetric advantages defenders still have, as Socket, Aikido, and StepSecurity routinely detect zero-day malicious packages in minutes.

This post is a reference for senior engineers and security teams: a chronology of the publicly disclosed incidents, a direct assessment of AI's role on both sides, and prescriptive configuration-level guidance covering local development, CI/CD, containers, Kubernetes, serverless, and incident response.

TL;DR#

  • npm's 2025-2026 attack wave moved from phishing and credential theft to self-replicating worms, poisoned CI runners, and malicious packages with valid provenance attestations.
  • AI changed the threat model in both directions: attackers abused local coding assistants and targeted LLM/MCP credentials, while defenders used behavioral scanning and CI egress controls to catch attacks within minutes or hours.
  • Provenance is necessary but not enough. Consumers also need workflow, branch, and environment pinning, plus cooldown windows before accepting fresh package releases.
  • The practical defense stack is layered: frozen lockfiles, disabled install scripts, private registry routing, SHA-pinned Actions, OIDC publishing, Harden-Runner egress blocking, signed images, restricted pods, default-deny network policy, and a runner-first incident response plan.

1. The 2025–2026 incident chronology#

The campaigns below form a clear escalation curve: from one-shot credential theft to manual worms requiring install-script execution, then to OIDC trusted-publisher abuse that produced valid SLSA Level 3 provenance attestations on malicious tarballs.

A desert surface split by glowing blue fracturesThe 2025-2026 incidents followed a clear escalation: stolen credentials, self-replicating malware, poisoned CI, and finally malicious packages with valid provenance.

s1ngularity / Nx — August 26–31, 2025#

A vulnerable pull_request_target GitHub Actions workflow in the Nx monorepo allowed bash injection through PR titles. The attacker exfiltrated an npm publish token through a webhook and published malicious versions of nx and several @nx/* packages with a postinstall: node telemetry.js hook (GHSA-cxm3-wv7p-598c).

The payload's defining novelty was its treatment of local AI assistants as privileged recon tools. It probed for locally authenticated Claude Code CLI, Gemini CLI, and Amazon Q CLI binaries, then invoked them with permission-bypass flags (--dangerously-skip-permissions, --yolo, --trust-all-tools) to enumerate wallets, credentials, and .env files on the victim's filesystem, writing results to /tmp/inventory.txt. It then created public GitHub repos named s1ngularity-repository[-0/-1] containing triple-base64 results.b64, and as a wiper appended sudo shutdown -h 0 to the victim's shell rc files. GitGuardian's post-incident analysis counted 2,349 distinct credentials harvested from 1,079 compromised systems, 82,901 total secrets across affected repositories, and 11,168 still-valid secrets; it also found roughly 10,767 repositories made public during the second wave. Wiz's updated reporting put the visible blast radius at 400+ users or organizations and 5,500+ repositories. Wiz also observed that Claude's guardrails frequently refused the recon prompts, while Gemini was more permissive.

The Nx "s1ngularity" Attack: Inside the Credential Leak
GitGuardian Blog - Take Control of Your Secrets Security faviconGitGuardian Blog - Take Control of Your Secrets Security

The Nx "s1ngularity" Attack: Inside the Credential Leak

On August 26, 2025, Nx, the popular build platform with millions of weekly downloads, was compromised with credential-harvesting malware. Using GitGuardian's monitoring data, we analyzed the exfiltrated credentials and reconstructed a fuller scope of exposure.

blog.gitguardian.com/the-nx-s1ngularity-attack-inside-the...
(opens in new tab)

qix / chalk / debug — September 8, 2025#

A spear-phishing email from the lookalike domain npmjs.help (registered Sept 4–5) hit maintainer Josh Junon ("Qix") with a 2FA-renewal lure that captured both password and a live TOTP via an adversary-in-the-middle proxy. Within minutes the attacker pushed malicious versions of 18 packages including chalk, debug, ansi-styles, supports-color, strip-ansi, color-convert, color-name, and chalk-template — totaling roughly 2.6 billion weekly downloads. Unlike the worm campaigns, the payload was a browser-side cryptocurrency stealer using Levenshtein-distance address substitution against window.ethereum, fetch, and XMLHttpRequest. Aikido Security flagged the compromise at 13:16 UTC, within minutes of the first malicious publishes; npm pulled tarballs within roughly two hours; Semgrep shipped a free detection rule the same afternoon. The case became the canonical "how fast can the ecosystem respond" benchmark.

Shai-Hulud — September 15–16, 2025#

The first self-replicating npm worm. Patient zero was rxnt-authentication; within hours the worm jumped to @ctrl/tinycolor, tinycolor, ngx-bootstrap, several CrowdStrike-namespace packages, and ultimately 500+ versions across 180+ packages (Socket count). Each compromised tarball carried a postinstall: node bundle.js script (~3.6 MB, multiple variants with confirmed SHA-256 hashes including 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09) that downloaded the TruffleHog secret scanner, harvested AWS/GCP/Azure/npm/GitHub credentials plus IMDS responses, exfiltrated to webhook[.]site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7 and to public GitHub repos named Shai-Hulud, then republished the worm to every other package owned by the compromised maintainer via registry.npmjs.org/-/v1/search?text=maintainer:<user>. It also wrote a .github/workflows/shai-hulud-workflow.yml for persistence. CISA issued an emergency alert on September 23, 2025. Socket's AI-powered scanner first flagged the malicious versions; ReversingLabs and StepSecurity provided early analysis.

Widespread Supply Chain Compromise Impacting npm Ecosystem | CISA
Cybersecurity and Infrastructure Security Agency CISA faviconCybersecurity and Infrastructure Security Agency CISA

Widespread Supply Chain Compromise Impacting npm Ecosystem | CISA

cisa.gov/news-events/alerts/2025/09/23/widespread-supply-...
(opens in new tab)

Shai-Hulud 2.0 / "The Second Coming" — November 23–24, 2025#

A more sophisticated variant compromised roughly 796 packages and 1,092 unique versions across Zapier, PostHog, Postman SDKs, AsyncAPI, and other maintainers. It moved execution from postinstall to preinstall to defeat defenses that only protect post-install hooks in some package managers, used Bun as the runtime (setup_bun.js -> bun_environment.js, hashes 62ee164b..., f099c5d9..., cbb9bc5a...), harvested credentials in parallel across cloud providers, exfiltrated to public GitHub repos with 18-character random names and description "Sha1-Hulud: The Second Coming.", and registered a self-hosted GitHub Actions runner named SHA1HULUD plus a .github/workflows/discussion.yaml workflow listening for on: discussion events as a command channel.

Its signature turn was persistence through GitHub Actions runners, paired with destructive behavior that made containment order matter. The clearest documented rm -rf ~/ token-revocation failsafe appears in the later Mini Shai-Hulud/TanStack campaign, but Shai-Hulud 2.0 still forced responders to treat self-hosted runners as active compromise infrastructure. The campaign was disclosed by Datadog Security Labs, Wiz, Socket, StepSecurity, and Microsoft Security on December 9, 2025.

SANDWORM_MODE — February 20, 2026#

Socket.dev's Threat Research Team disclosed a Shai-Hulud derivative spreading through 19 typosquatted packages (including suport-color and typosquats of claude-code) published under the aliases official334 and javaorg. Beyond standard credential theft, it introduced a more explicit AI-toolchain attack surface: it harvested API keys for multiple commercial LLM providers, probed common local LLM endpoints, and injected a malicious MCP server with embedded prompt-injection logic into AI coding assistants on the infected host. Socket reported that the sample contained dormant code paths suggesting future polymorphic self-rewriting via a local LLM.

SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflow...
Socket faviconSocket

SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflow...

An emerging npm supply chain attack that infects repos, steals CI secrets, and targets developer AI toolchains for further compromise.

socket.dev/blog/sandworm-mode-npm-worm-ai-toolchain-poiso...
(opens in new tab)

Mini Shai-Hulud / TanStack — May 11–13, 2026#

The most architecturally significant incident in the series hit TanStack packages including @tanstack/react-router (12M weekly downloads). The compromise combined a pull_request_target "Pwn Request" with GitHub Actions cache poisoning and OIDC token extraction from runner memory (GHSA-g7cv-rxg3-hmpx; CVE-2026-45321, CVSS 9.6). Because the worm published using the victim's OIDC trusted-publisher binding, npm generated valid Sigstore provenance and SLSA Build Level 3 attestations for the malicious tarballs, defeating provenance-based defenses for any consumer not also pinning the workflow path. Persistence included a gh-token-monitor daemon with an rm -rf ~/ failsafe; Russian-language locales were skipped; C2 used a git-tanstack.com typosquat and Session messenger. TanStack's postmortem, StepSecurity, Snyk, and Rescana covered the incident; it pushed the ecosystem to require branch and environment pinning on trusted publishers, not just repo and workflow filename.

Postmortem: TanStack npm supply-chain compromise | TanStack Blog
tanstack.com favicontanstack.com

Postmortem: TanStack npm supply-chain compromise | TanStack Blog

On 2026-05-11, an attacker chained a pull_request_target Pwn Request, GitHub Actions cache poisoning across the fork↔base trust boundary, and OIDC token extraction from runner memory to publish 84 malicious versions across 42 @tanstack/* packages on npm. Full postmortem.

tanstack.com/blog/npm-supply-chain-compromise-postmortem
(opens in new tab)

Adjacent incidents that shaped controls#

The tj-actions/changed-files compromise (CVE-2025-30066, March 2025) retroactively rewrote tags across 23,000+ repositories — the proximate reason every hardened workflow now SHA-pins third-party actions. The Bitwarden CLI / Checkmarx ast-github-action compromise on April 23, 2026, demonstrated that even security vendors' own actions are attack surface. Postmark MCP packages, rspack, and a steady drip of phishing and dependency-confusion incidents filled the gaps. Aggregate impact: Sonatype 512,847 malicious packages in 2024 (+156%), ReversingLabs +73% in 2025, Socket flagging 6,000+ malicious npm packages in June 2025 alone.


2. AI's dual role — attacker tool, defender's edge#

How attackers are using AI#

The strongest public sources — Anthropic's Detecting and Countering Misuse of AI (August 2025) and Disrupting the First Reported AI-Orchestrated Cyber Espionage Campaign (November 13, 2025), OpenAI's quarterly Disrupting Malicious Uses of AI series, Google Threat Intelligence Group's Adversarial Misuse of Generative AI (January 2025) and its November 2025 update, and the Microsoft Digital Defense Report 2025 — converge on four patterns relevant to npm defenders.

A black mask split down the center with blue and amber light in the eyesAI changed both sides of the npm threat model: attackers use it for scale and recon, while defenders use it for behavioral detection.

First, AI-augmented phishing is the new baseline. Microsoft measured AI-written phishing at a 54% click-through rate versus 12% for human-written lures — a 4.5× multiplier — and estimated AI makes phishing up to 50× more profitable. CrowdStrike reported a 442% increase in vishing in H2 2024 and an 89% YoY rise in AI-enabled adversary operations in their 2026 report. The npmjs.help campaign that compromised Qix is the canonical npm-specific case: a credible 2FA-renewal email convincing enough to deceive a top-tier maintainer.

Second, "abuse of locally authenticated AI agents" is now a confirmed post-compromise tradecraft category. The s1ngularity payload's invocation of Claude/Gemini/Q CLIs with permission-bypass flags is documented in writeups from Wiz, StepSecurity, Snyk, Aikido, and GitGuardian. Defenders must treat AI coding agents as privileged identities — they have broad filesystem access, often cached session credentials, and frequently run with auto-approve flags on machines holding the keys to production.

Third, slopsquatting has crossed from theoretical to credible. The peer-reviewed USENIX Security 2025 paper We Have a Package for You! (Spracklen et al.) evaluated 16 LLMs across 576,000 code samples and measured commercial-model hallucination rates around 5.2% and open-source-model rates around 21.7%, totaling 205,474 unique fabricated package names. Hallucinations are repeatable: the same prompt class returns the same fake name, which makes pre-registration economically viable. Lasso Security registered an empty placeholder for the hallucinated huggingface-cli and received 30,000+ legitimate downloads in three months, including a paste into Alibaba's public README. A 2026 replication on frontier models compressed the spread to roughly 4.62%-6.10%, which is progress but not retirement of the threat. Vulcan Cyber's earlier 2023 work and Aikido's react-codeshift finding (propagated through 237 repos via LLM-generated Agent Skills) suggest the adoption path is real even before weaponization.

We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs
arXiv.org faviconarXiv.org

We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs

The reliance of popular programming languages such as Python and JavaScript on centralized package repositories and open-source software, combined with the emergence of code-generating Large Language Models (LLMs), has created a new type of threat to the software supply chain: package hallucinations. These hallucinations, which arise from fact-conflicting errors when generating code using LLMs, represent a novel form of package confusion attack that poses a critical threat to the integrity of the software supply chain. This paper conducts a rigorous and comprehensive evaluation of package hallucinations across different programming languages, settings, and parameters, exploring how a diverse set of models and configurations affect the likelihood of generating erroneous package recommendations and identifying the root causes of this phenomenon. Using 16 popular LLMs for code generation and two unique prompt datasets, we generate 576,000 code samples in two programming languages that we analyze for package hallucinations. Our findings reveal that that the average percentage of hallucinated packages is at least 5.2% for commercial models and 21.7% for open-source models, including a staggering 205,474 unique examples of hallucinated package names, further underscoring the severity and pervasiveness of this threat. To overcome this problem, we implement several hallucination mitigation strategies and show that they are able to significantly reduce the number of package hallucinations while maintaining code quality. Our experiments and findings highlight package hallucinations as a persistent and systemic phenomenon while using state-of-the-art LLMs for code generation, and a significant challenge which deserves the research community's urgent attention.

arxiv.org/abs/2406.10279
(opens in new tab)

Fourth, AI-embedded and AI-targeting malware has emerged. GTIG's November 2025 update named PROMPTSTEAL (APT28/FROZENLAKE) and PROMPTFLUX as the first observed malware families that query LLM APIs at runtime to generate just-in-time execution scripts rather than hard-coding them. SANDWORM_MODE then proved attackers will reach for local models — and harvest the keys, prompts, and MCP servers around them — as a deliberate strategy.

A caveat worth stating plainly: Anthropic's GTG-1002 disclosure (an alleged Chinese state-sponsored campaign in which Claude Code autonomously executed 80–90% of intrusion operations against ~30 targets) has been met with appropriate skepticism by parts of the security research community because few independent IOCs were published. The category-level risk stands regardless of any single incident's contested details.

How defenders are using AI#

The defensive AI stack has matured rapidly because traditional CVE-based SCA is structurally unable to catch these attacks — Shai-Hulud and chalk/debug never had CVEs at the moment of harm; their detection had to come from behavior.

The tools are easier to reason about by where they sit in the delivery path.

Registry and install-time blockers

  • Socket.dev uses AI/ML risk scoring across 70+ signals, install-script analysis, and the TypoSmart embedding model for typosquatting. Blocking is available through Socket Firewall's HTTP proxy.
  • Aikido Safe Chain wraps npm, yarn, and pnpm with Aikido Intel, including Phylum-derived malware signals. It can block at install time.
  • Veracode (Phylum) combines ML behavioral analysis with a catalog of roughly 500K malicious packages at the time of acquisition. Package Firewall provides real-time blocking.
  • JFrog Curation applies metadata and ML policy at the Artifactory proxy layer, including auto-substitution to mature versions. Blocking happens at the registry proxy.
  • packj combines static analysis, strace, and sandbox execution, calibrated to the 93.9% of malware that uses install scripts. It can run as an OSS install-time guard.

PR, workflow, and repository controls

  • Semgrep Supply Chain adds reachability analysis, claims 98% noise reduction, and provides LLM upgrade guidance. It can block during PR review.
  • GitHub Advanced Security combines CodeQL, Copilot Autofix (alerts resolve 3-12x faster), and secret push protection for AI tokens. Blocking is strongest at push time.
  • StepSecurity Harden-Runner uses eBPF on GitHub Actions runners with a SOC-curated global block list. It can block unexpected egress from CI jobs.
  • Snyk DeepCode + Agent Fix combines symbolic analysis and LLMs with CodeReduce scoping, with roughly 80% autofix accuracy reported. Treat it as PR-time advisory automation.
  • Endor Labs focuses on function-level reachability and AURI AI prioritization. Treat it as advisory prioritization for dependency risk.

Open analysis and intelligence feeds

  • OpenSSF Package Analysis detonates every new release in a gVisor sandbox and publishes results to OSV. It is an OSS intelligence feed rather than a direct blocker.
  • Datadog GuardDog combines Semgrep, YARA, and metadata heuristics across six ecosystems. It is an OSS advisory scanner.

Real detection wins from the campaigns above: Aikido caught chalk/debug within minutes; Socket's AI scanner flagged s1ngularity and Shai-Hulud shortly after the first malicious publishes; StepSecurity's Harden-Runner caught tj-actions, axios RAT, Trivy v0.69.4, and Shai-Hulud 2.0 in CNCF Backstage because the compromised packages contacted endpoints the runners had never reached in baseline telemetry. The realistic detection window for downstream defenders is hours, not days.

LLM-based code review for dependencies, however, is not yet a substitute for human review. Endor Labs' Henrik Plate ran 1,800 binary classifications across GPT-3.5 and Vertex AI text-bisonagreement was 89% but accuracy was only 3.4–7.9%, with minified/packed JavaScript producing the worst results. Worse, malicious packages can embed comments that instruct the LLM reviewer to ignore findings (a class-A prompt injection per OWASP LLM01:2025). The right pattern is to strip comments and literals, deobfuscate first, use program-analysis scoping, and treat LLM output as one signal alongside sandbox and heuristic outputs.


3. Hardening the local developer environment#

Every npm attack in this report executed because a developer or CI runner ran npm install against a compromised version. The single most impactful defense is making install boring: deterministic, scriptless, sandboxed, and signature-verified.

A translucent protective dome over a desert structure at duskLocal installs should run inside guardrails: no ambient credentials, no uncontrolled lifecycle scripts, and no direct path around registry policy.

Lockfile discipline is non-negotiable. Use frozen/immutable installs everywhere — npm ci --ignore-scripts, pnpm install --frozen-lockfile, yarn install --immutable. Never let install mutate the lockfile silently. The default mode for an npm-supply-chain-aware project's .npmrc should be:

INI
# .npmrc — commit this
registry=https://npm.mycompany.com/
@mycompany:registry=https://npm.mycompany.com/
ignore-scripts=true
fund=false
audit=true
audit-level=high
save-exact=true
package-lock=true
replace-registry-host=always
engine-strict=true
//npm.mycompany.com/:_authToken=${NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPMJS_TOKEN}

In npm CLI 11.16+, the allow-* install controls close common bypass paths around the registry: allow-git=none blocks Git dependencies, allow-remote=none blocks direct tarball URLs, allow-file=none blocks local tarball files, and allow-directory=none blocks directory dependencies. The same release also adds min-release-age, a cooldown control that only accepts versions published more than the given number of days ago.

Use the stricter controls on the install command:

Bash
npm ci --ignore-scripts \
  --allow-git=none \
  --allow-remote=none \
  --allow-file=none \
  --allow-directory=none \
  --min-release-age=3

Each allow-* control also supports root if you want to allow only direct specs declared in the root package.json, but none is the strictest default for CI and production-bound repos. Use a longer min-release-age window, such as 7 days, for routine dependency-update jobs; keep a documented break-glass path for urgent security fixes. Changing these settings does not remove anything already installed, so pair them with a lockfile audit:

Bash
if npm query ":type(git)" --json | jq -e 'length > 0'; then
  echo "::error::Git dependencies bypass registry policy"
  npm query ":type(git)" | jq -r '.[].name'
  exit 1
fi

For pre-install checks, also reject Git URL, hosted-Git shortcut, remote tarball, local tarball, and directory specs in package.json and package-lock.json during CI review.

--ignore-scripts is the single highest-ROI flag in the ecosystem. A 2022 academic survey found ~94% of malicious npm packages use install scripts; Shai-Hulud V1, rspack, and Bitwarden CLI all relied on them. The pnpm 10+ default already blocks lifecycle scripts, allow-listed via pnpm-workspace.yaml:

YAML
onlyBuiltDependencies:
  - esbuild
  - "@swc/core"
  - sharp
neverBuiltDependencies:
  - core-js

Yarn Berry uses enableScripts: false in .yarnrc.yml. Shai-Hulud 2.0's switch to preinstall is a reminder to disable scripts comprehensively, not just postinstall. Pair with @lavamoat/allow-scripts for projects that genuinely need native builds.

Verify signatures every install. npm audit signatures (npm CLI ≥ 9.5.0) cryptographically verifies registry ECDSA signatures and Sigstore provenance attestations against each downloaded tarball. Run it as a hard gate in CI:

Bash
npm ci --ignore-scripts
npm audit signatures
npm audit --audit-level=high

Pin to a private registry and a scope. Verdaccio, JFrog Artifactory, Sonatype Nexus, or GitHub Packages can proxy npm with caching, allowing you to enforce cooldown windows and deny known-bad versions at the boundary. Scope pinning neutralizes dependency confusion, where an attacker publishes a higher version of your internal package name on the public registry.

Sandbox the install step. Dev Containers are the right default after Shai-Hulud — they isolate node_modules execution from the host's ~/.aws, ~/.ssh, and credential stores. A hardened .devcontainer/devcontainer.json:

JSON
{
  "image": "mcr.microsoft.com/devcontainers/typescript-node:24-bookworm",
  "runArgs": [
    "--cap-drop=ALL",
    "--security-opt=no-new-privileges",
    "--read-only",
    "--tmpfs=/tmp:rw,size=512m"
  ],
  "mounts": [
    "source=${localWorkspaceFolderBasename}-node-home,target=/home/node,type=volume"
  ],
  "containerEnv": {
    "NPM_CONFIG_IGNORE_SCRIPTS": "true",
    "NPM_CONFIG_AUDIT_LEVEL": "high",
    "NPM_CONFIG_CACHE": "/home/node/.npm"
  },
  "containerUser": "node",
  "remoteUser": "node",
  "postCreateCommand": "npm ci --ignore-scripts && npm audit signatures"
}

For one-off installs, Bubblewrap can run npm with a read-only view of the host filesystem, a masked home directory, and a writable workspace mounted at /work:

Bash
bwrap \
  --ro-bind / / \
  --tmpfs "$HOME" \
  --bind "$PWD" /work \
  --chdir /work \
  --tmpfs /tmp \
  --unshare-all \
  --share-net \
  --setenv HOME "$HOME" \
  -- \
  npm ci --ignore-scripts

Eliminate plaintext tokens. Pull npm tokens just-in-time from a secret manager rather than persisting them in ~/.npmrc. The newer granular access tokens default to a 7-day expiry (90-day max) as of October 2025; legacy classic tokens are being revoked. Enforce WebAuthn 2FA, not TOTP — the npmjs.help campaign defeated TOTP-only accounts in real time.


4. CI/CD pipeline hardening#

CI/CD is where supply chain attacks become catastrophic because runners hold publish tokens, cloud OIDC trust, and the ability to overwrite production artifacts.

SHA-pin every third-party action. The tj-actions incident retroactively rewrote tags. Use the full 40-char SHA with a tag comment:

YAML
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0

Use automation so digest pinning does not become a manual chore. Renovate is the stronger tool for this specific control because helpers:pinGitHubActionDigests can convert tag-based action references into full commit SHAs and preserve the intended version as a comment:

JSON
{
  "extends": [
    "config:recommended",
    "helpers:pinGitHubActionDigests"
  ],
  "packageRules": [{
    "matchManagers": ["github-actions"],
    "groupName": "GitHub Actions"
  }]
}

That produces the pattern you want in workflow files:

YAML
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

The comment matters. Renovate can update the SHA according to the tag in the comment; a bare SHA with no version comment is disabled by default because the bot cannot know which branch or tag the digest was meant to follow. Dependabot should still be enabled for GitHub Actions version updates, but treat it as update automation rather than a pinning policy engine:

YAML
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5
    labels:
      - dependencies
      - github-actions

If you standardize on Dependabot alone, add a separate policy check that rejects unpinned third-party actions during review. Dependabot can monitor owner/repo@ref and owner/repo@commit-sha action references, but it is not a replacement for Renovate's digest-pinning preset.

Default-deny GITHUB_TOKEN permissions at the org level and re-grant per-job:

YAML
permissions:
  contents: read   # workflow default
jobs:
  publish:
    permissions:
      contents: read
      id-token: write    # only on the publish job
      attestations: write

Migrate every npm publish to Trusted Publishers (OIDC). GA on July 31, 2025; npm CLI ≥ 11.5.1 required. After binding the package to a specific repo, workflow filename, branch, and environment in the npm UI (Mini Shai-Hulud proved unpinned bindings are still exploitable), the publish job needs no NODE_AUTH_TOKEN and provenance is emitted implicitly. A full hardened release workflow combining everything:

YAML
name: Release to npm
on:
  push: { tags: ['v*.*.*'] }
permissions: { contents: read }
jobs:
  publish:
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read
      id-token: write
      attestations: write
    steps:
      - uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176
        with:
          egress-policy: block
          disable-sudo-and-containers: true
          allowed-endpoints: >
            api.github.com:443
            github.com:443
            registry.npmjs.org:443
            fulcio.sigstore.dev:443
            rekor.sigstore.dev:443
            tuf-repo-cdn.sigstore.dev:443
            nodejs.org:443
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
        with: { persist-credentials: false }
      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
        with: { node-version: '22.14.0', registry-url: 'https://registry.npmjs.org' }
      - run: npm install -g npm@latest
      - run: npm ci --ignore-scripts
      - run: npm audit signatures
      - run: npm audit --audit-level=high
      - run: npm run build && npm test
      - run: npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file bom.json --omit dev
      - uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b
        with: { subject-path: '*.tgz', sbom-path: 'bom.json' }
      - run: npm publish --access public   # OIDC + provenance implicit

Dependabot cooldown is now a primary defense. Released July 2025, the cooldown option delays merging freshly published versions long enough for the ecosystem to detect compromises (most are caught in <24h):

YAML
updates:
  - package-ecosystem: "npm"
    cooldown:
      default-days: 7
      semver-major-days: 30
      semver-minor-days: 7
      semver-patch-days: 3

Renovate's equivalent — minimumReleaseAge: "7 days" plus the security:minimumReleaseAgeNpm preset (default-on since v42) — pairs with vulnerabilityAlerts (minimumReleaseAge: "0 days") so security fixes bypass cooldown.

Generate SBOMs and signed attestations on every release with @cyclonedx/cyclonedx-npm or syft scan dir:. -o spdx-json=sbom.spdx.json, attaching them via actions/attest-sbom. OpenSSF Scorecard running weekly catches workflow misconfigurations and missing pinned dependencies before they're exploited. The SLSA generic generator produces L3 provenance for non-npm artifacts.


5. Incident response playbook#

Detect quickly with focused commands. Across node_modules and .github/, grep for the high-signal IOC strings: webhook.site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7, checkethereumw, shai-hulud-workflow, SHA1HULUD, s1ngularity-repository, --dangerously-skip-permissions, --trust-all-tools, trufflehog. Hunt named payload files (bundle.js, telemetry.js, setup_bun.js, bun_environment.js, router_init.js, results.b64). Validate with npm audit signatures. Diff lockfiles against git history for unexpected version pins.

Org-wide GitHub audit with gh:

Bash
ORG=acmeinc
gh api search/repositories -f q="org:$ORG Shai-Hulud in:name"
gh api search/repositories -f q="org:$ORG s1ngularity-repository in:name"
gh api search/repositories -f q="org:$ORG \"Sha1-Hulud: The Second Coming\" in:description"
gh api search/code -f q="org:$ORG filename:shai-hulud-workflow.yml"
gh api search/code -f q="org:$ORG path:.github/workflows discussion.yaml"
gh api "orgs/$ORG/audit-log?phrase=action:repo.create+created:>2025-09-14" --paginate
gh api "orgs/$ORG/audit-log?phrase=action:repo.add_self_hosted_runner"
gh api "orgs/$ORG/audit-log?phrase=action:public_key.create"

s1ngularity exfiltrated to user repositories, not org repos, so iterate over members and check each user account.

Contain in this order:

  1. Quarantine affected hosts, but do not power them off; preserve forensic state first.
  2. Delete any SHA1HULUD self-hosted runners and remove discussion.yaml / shai-hulud-workflow.yml.
  3. Suspend the compromised user account.
  4. Rotate credentials leaf-to-root: revoke npm tokens, rotate GitHub PATs and OAuth grants, delete SSH and GPG keys, rotate AWS access keys (aws iam delete-access-key), GCP service account keys, and Azure service principal credentials.
  5. Revoke active sessions everywhere.
  6. For malicious versions of your packages, use npm unpublish within the 72-hour window or npm deprecate with a security message that points to your GHSA.
  7. Purge caches: npm cache clean --force, pnpm store prune, GitHub Actions caches, Docker layer caches, and CDN edge caches.
  8. Re-pin lockfiles to a known-good version and reinstall with --ignore-scripts.

Forensic audit assumes everything readable by the install process was leaked — TruffleHog scans for 800+ secret types and Shai-Hulud, s1ngularity, and TanStack all used it. Build a credential-to-resource map for each leaked key and query CloudTrail / Cloud Logging / Azure Activity for unexpected usage after the publish date. Check git log --all --since=<date> --diff-filter=A -- '.github/workflows/*.yml' for injected workflows; git log -p --all -S 'shai-hulud-workflow' -S 'trufflehog' -S 'webhook.site' for committed payloads.

Communicate with users via a draft private GHSA (GitHub is a CNA — publishing the advisory auto-issues a CVE). The deprecation message is your most visible warning channel because it shows on every npm install:

Text
npm deprecate "yourpkg@>=1.2.3 <1.2.5" \
"SECURITY: versions 1.2.3-1.2.4 were compromised on 2026-MM-DD and contain a credential-stealing script. Upgrade to >=1.2.5 immediately and rotate any credentials accessible to environments that installed an affected version. Details: https://github.com/org/repo/security/advisories/GHSA-xxxx"

Post-incident hardening comes down to five mandates:

  1. Migrate to npm Trusted Publishers with branch and environment pinning.
  2. Enforce WebAuthn 2FA; do not rely on TOTP alone.
  3. Default to ignore-scripts=true everywhere.
  4. Eliminate pull_request_target, or harden every remaining use.
  5. Instrument every workflow with Harden-Runner.

6. Bonus how-to: routing all npm traffic through JFrog Artifactory (consume + publish)#

This section is a complete, opinionated walkthrough for an organization that uses npm (not pnpm/yarn) and wants every dependency install and every internal package publish to flow through JFrog Artifactory, with no path to registry.npmjs.org ever reaching a developer machine or CI runner. It assumes the JFrog-only model, where internal packages live solely in your JFrog local repo. Switch to the dual-publish variant noted in section 7.5 only if you ship customer-facing SDKs on public npm.

An aerial view of a gate channeling glowing blue paths across a desert surfaceA registry proxy is useful only when every path is forced through it: installs, publishes, bots, CI, and containerized builds.

6.1 Repository layout in Artifactory#

Stand up three repos behind one virtual:

  • npm-remote — proxy of https://registry.npmjs.org, caching enabled, with JFrog Curation / Xray policies attached
  • npm-local — your internal packages, write-restricted to the CI publish identity
  • npm-virtual — what every developer and runner points at; aggregates the above

Critical settings on the virtual repo:

  • Default Deployment Repository: npm-local (so npm publish against the virtual lands in the right place)
  • Resolution order: local first, then remote — this is what kills dependency confusion attacks targeting your @mycompany/* scope
  • On npm-local: enable "Handle Releases", disable "Handle Snapshots" unless you want -SNAPSHOT versions, enable "Block Mismatching Mime Types", and attach an Xray policy that blocks unsigned artifacts and known-malicious versions
  • On npm-remote: turn on JFrog Curation if licensed — it blocks known-malicious public packages at the proxy layer before they ever reach a developer's machine

A defensible alternative is to point @mycompany:registry= at npm-local directly rather than the virtual repo. That removes any fallback path for internal scopes at the cost of split config. Most teams find the virtual-with-priority-resolution approach simpler in practice; either is fine.

6.2 Consumer-side configuration (.npmrc)#

Two .npmrc files matter. The project-level .npmrc is committed to the repo:

INI
registry=https://artifactory.mycompany.com/api/npm/npm-virtual/
@mycompany:registry=https://artifactory.mycompany.com/api/npm/npm-local/
ignore-scripts=true
audit=false
fund=false
package-lock=true
save-exact=true
engine-strict=true
replace-registry-host=always

Keep this file consumer-only. replace-registry-host=always keeps newly written lockfile tarball URLs pointed at Artifactory instead of the public registry. npm 11 warns on scoped always-auth, and publishConfig:* entries in .npmrc do not control npm publish; publish targets belong in each package's package.json, shown in §6.6.

A note on audit=false: npm audit calls registry.npmjs.org directly by default. If you leave audit on without configuring audit-registry, you will see traffic to npmjs.org and incorrectly conclude you are leaking, when it is audit metadata. Disable npm audit at the CLI and use JFrog Xray for vulnerability scanning instead.

Developer auth should be JFrog CLI-managed, not hand-written into ~/.npmrc. First-time setup on a developer machine:

Bash
# Interactive browser login; stores credentials in JFrog CLI's local config.
jf login
jf c show
jf c use mycompany

# Run once per project to bind npm resolution and deploy repos.
jf npm-config \
  --server-id mycompany \
  --repo-resolve npm-virtual \
  --repo-deploy npm-local

# Use the JFrog wrapper so npm auth is obtained from Artifactory on demand.
jf npm ci --ignore-scripts

With this flow, JFrog CLI stores the platform login locally and obtains npm credentials through Artifactory when jf npm ... runs; developers do not paste long-lived npm tokens into a user-level .npmrc. If a tool cannot run through jf npm, generate npm-readable auth at session start from JFrog CLI or your secret manager and delete it after use. For non-interactive automation, prefer JFrog OIDC or a short-lived Reference Token scoped narrowly (deploy/cache on npm-local only for publishers; read on npm-virtual for consumers). Never use the legacy "API Key" — it's account-wide and revocation is all-or-nothing.

6.3 The leaks that bypass .npmrc — fixing them is mandatory#

A correctly configured .npmrc is not sufficient. Five things can still reach registry.npmjs.org unless explicitly handled:

Lockfile-embedded URLs. If package-lock.json was generated against the public registry, resolved fields can still contain public-registry URLs. replace-registry-host=always helps newly written locks follow the configured registry, but do not trust inherited lockfiles. Regenerate after the registry switch:

Bash
rm -rf node_modules package-lock.json
npm cache clean --force
npm ci
grep -c "registry.npmjs.org" package-lock.json   # must return 0

Transitive publishConfig.registry and direct URL deps in package.json — search the tree for "registry": and explicit https://registry.npmjs.org strings and fix any hits.

npx, corepack, npm-check-updates, npm view — each has its own registry behavior. Test each on a clean machine after rollout.

Renovate needs explicit hostRules and registryUrls in renovate.json:

JSON
{
  "hostRules": [{
    "matchHost": "artifactory.mycompany.com",
    "hostType": "npm",
    "token": "{{ secrets.ARTIFACTORY_TOKEN }}"
  }],
  "packageRules": [{
    "matchDatasources": ["npm"],
    "registryUrls": ["https://artifactory.mycompany.com/api/npm/npm-virtual/"]
  }],
  "minimumReleaseAge": "7 days"
}

Dependabot needs .github/dependabot.yml with a registries: block:

YAML
version: 2
registries:
  jfrog-npm:
    type: npm-registry
    url: https://artifactory.mycompany.com/api/npm/npm-virtual/
    token: ${{ secrets.ARTIFACTORY_TOKEN }}
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule: { interval: "weekly" }
    registries: ["jfrog-npm"]
    cooldown:
      default-days: 7
      semver-major-days: 30
      semver-minor-days: 7
      semver-patch-days: 3

6.4 Enforcement — the only way to be sure#

On developer laptops, push a corporate-proxy / ZTNA policy denying registry.npmjs.org, *.npmjs.com, *.npmjs.org and forcing traffic through Artifactory.

In GitHub Actions, deliberately omit npmjs from the allow list — if any tool tries to reach it, the build fails loudly:

YAML
- uses: step-security/harden-runner@v2
  with:
    egress-policy: block
    allowed-endpoints: >
      artifactory.mycompany.com:443
      api.github.com:443
      github.com:443
      nodejs.org:443

In Kubernetes/containerized builds, the equivalent NetworkPolicy or Cilium FQDN policy denies *.npmjs.org outbound.

In Docker builds, fail the build if leakage is detected:

Dockerfile
ARG NPM_REGISTRY=https://artifactory.mycompany.com/api/npm/npm-virtual/
RUN npm config set registry "$NPM_REGISTRY" \
 && npm config set ignore-scripts true \
 && npm ci \
 && ! grep -q "registry.npmjs.org" /app/package-lock.json

A CI guardrail to catch regressions in lockfiles and .npmrc:

YAML
- name: Verify no npmjs.org references
  run: |
    if grep -rE "registry\.npmjs\.(org|com)" package-lock.json .npmrc; then
      echo "::error::npmjs.org reference found — fix registry config"
      exit 1
    fi

Sanity check on any clean machine post-rollout:

Bash
npm ci -ddd 2>&1 | grep -i "GET https" | grep -v artifactory   # must return nothing

6.5 Publishing internal packages — the structural choice#

Two viable models for internal packages:

Model A — JFrog-only. Packages live solely in npm-local. Provenance is JFrog-native (Build-Info, Xray, GitHub-native attestations). Simpler; what most enterprises with JFrog actually do.

Model B — Dual publish. Packages go to JFrog and to public npm under a @mycompany scope with npm Trusted Publishers providing Sigstore provenance. Only makes sense for customer-facing SDKs installed from public npm.

The rest of this section assumes Model A.

6.6 Per-package publish configuration#

In each publishable package's package.json, mirror the publish target so package-level config wins over anything ambient:

JSON
{
  "name": "@mycompany/widget",
  "version": "1.4.2",
  "publishConfig": {
    "registry": "https://artifactory.mycompany.com/api/npm/npm-local/",
    "access": "restricted"
  },
  "files": ["dist/", "README.md", "LICENSE"],
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

The files allow-list matters more than people think — it prevents accidental publishing of .env, .npmrc, test fixtures with secrets, or node_modules/. Pair with a .npmignore. Always dry-run before a real publish and diff the file list against the previous release; a sudden new telemetry.js / setup.js / postinstall.js is exactly the signature you're hunting for. Make this a CI check that fails if unexpected files appear.

Bash
npm publish --dry-run
npm pack --dry-run --json | jq -r '.[0].files[].path' | sort

6.7 Authentication for CI publishes — never long-lived tokens#

This is the part that bit Nx and TanStack hardest. Two options, in preference order:

Option 1 — JFrog OIDC integration (GA mid-2024, strongly preferred). Configure an OIDC identity mapping in Artifactory that trusts your GitHub Actions identity. The runner exchanges its OIDC token for a short-lived Artifactory access token at publish time. No long-lived JFROG_TOKEN secret lives in CI:

YAML
- uses: jfrog/setup-jfrog-cli@v4
  env:
    JF_URL: https://artifactory.mycompany.com
  with:
    oidc-provider-name: github-actions-publisher
    oidc-audience: jfrog-github

Scope the OIDC mapping on the Artifactory side to deploy/cache on npm-local only, bound to a specific repo, workflow filename, branch, and environment — the same lesson Mini Shai-Hulud taught for npm Trusted Publishers.

Option 2 — Reference Token (if OIDC isn't available yet). Create a Reference Token scoped to deploy/cache on npm-local, expiration in hours, stored in a GitHub Environment secret (not a repo secret) with required reviewers on the release environment.

6.8 The hardened publish workflow#

Putting consumer config, leak prevention, OIDC auth, tarball inspection, SBOM, and attestation together:

YAML
name: Publish to JFrog
on:
  push:
    tags: ['v*.*.*']

permissions:
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: release          # required reviewers + scoped secrets
    env:
      BUILD_NAME: ${{ github.event.repository.name }}
      BUILD_NUMBER: ${{ github.run_number }}
      NPM_CONFIG_IGNORE_SCRIPTS: "true"
    permissions:
      contents: read
      id-token: write             # for JFrog OIDC + GitHub attestations
      attestations: write

    steps:
      - uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176
        with:
          egress-policy: block
          disable-sudo-and-containers: true
          allowed-endpoints: >
            artifactory.mycompany.com:443
            api.github.com:443
            github.com:443
            objects.githubusercontent.com:443
            token.actions.githubusercontent.com:443
            nodejs.org:443
            releases.jfrog.io:443
            fulcio.sigstore.dev:443
            rekor.sigstore.dev:443
            tuf-repo-cdn.sigstore.dev:443

      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
        with: { persist-credentials: false }

      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
        with:
          node-version: '24.13.0'

      - uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5
        env:
          JF_URL: https://artifactory.mycompany.com
        with:
          oidc-provider-name: github-actions-publisher
          oidc-audience: jfrog-github

      - name: Configure JFrog npm integration
        run: |
          jf rt ping
          jf npm-config \
            --server-id-resolve=setup-jfrog-cli-server \
            --repo-resolve=npm-virtual \
            --server-id-deploy=setup-jfrog-cli-server \
            --repo-deploy=npm-local

      - name: Verify no public-registry leakage
        run: |
          if grep -qE "registry\.npmjs\.(org|com)" package-lock.json .npmrc 2>/dev/null; then
            echo "::error::Public registry reference detected"; exit 1
          fi

      - run: jf npm ci --build-name="$BUILD_NAME" --build-number="$BUILD_NUMBER"
      - run: npm run build
      - run: npm test

      - name: Pack and inspect publish payload
        id: pack
        run: |
          npm pack --json > pack.json
          jq -r '.[0].files[].path' pack.json | sort
          if jq -e '.[0].files[] | select(.path | test("(^|/)\\.env($|\\.)|(^|/)\\.npmrc$|(^|/)node_modules/|\\.(key|pem)$"))' pack.json; then
            echo "::error::Suspicious file in publish payload"; exit 1
          fi
          echo "tarball=$(jq -r '.[0].filename' pack.json)" >> "$GITHUB_OUTPUT"

      - name: Generate SBOM
        run: ./node_modules/.bin/cyclonedx-npm --output-format JSON --output-file bom.json --omit dev

      - uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b
        with:
          subject-path: ${{ steps.pack.outputs.tarball }}
          sbom-path: 'bom.json'

      - name: Publish
        run: jf npm publish "${{ steps.pack.outputs.tarball }}" --build-name="$BUILD_NAME" --build-number="$BUILD_NUMBER"
        # JFrog CLI uses the OIDC-backed server config from setup-jfrog-cli.
        # GitHub-native attestations above provide the Sigstore-backed proof consumers verify.

      - name: Upload SBOM and build-info to JFrog
        run: |
          jf rt upload bom.json "npm-local/@mycompany/widget/-/sbom-${GITHUB_REF_NAME}.json"
          jf rt build-publish "$BUILD_NAME" "$BUILD_NUMBER"

Three things worth calling out:

  • environment: release is what gates OIDC token issuance behind required reviewers. Without it, anyone who can push a tag can publish.
  • Use jf npm, not raw npm, for install and publish. JFrog CLI owns the OIDC-backed Artifactory auth and records build-info for both operations.
  • Tarball inspection is your fail-safe against both accidental and malicious file inclusion. Treat it as mandatory.
  • The SBOM generator must already be installed. Add @cyclonedx/cyclonedx-npm as a dev dependency or use an internally mirrored equivalent; do not fetch it with npx --yes during the publish job.
  • GitHub-native attestations are the JFrog-compatible equivalent of npm provenance — verifiable proof that this tarball was built by this workflow at this commit, anchored in Sigstore. Consumers verify with gh attestation verify.

6.9 Consumer-side attestation verification#

Publishing attestations without verification does not help. Make consuming projects' CI verify attestations on internal packages, pinned to the workflow path. That defeats the Mini Shai-Hulud class of OIDC-forged provenance:

YAML
- name: Verify internal package attestations
  run: |
    for pkg in $(jq -r '.dependencies | keys[] | select(startswith("@mycompany/"))' package.json); do
      tarball=$(npm pack "$pkg" --silent)
      gh attestation verify "$tarball" \
        --repo "mycompany/${pkg#@mycompany/}" \
        --predicate-type "https://slsa.dev/provenance/v1" \
        --signer-workflow ".github/workflows/publish.yml" \
        || { echo "Attestation failed for $pkg"; exit 1; }
    done

6.10 Pragmatic rollout order#

  1. Stand up Artifactory virtual + remote + local repos with the settings in §6.1
  2. Update project .npmrc and regenerate lockfiles in one branch — verify with grep
  3. Add the CI grep guardrail (§6.4)
  4. Configure Dependabot/Renovate to use the virtual repo with cooldown
  5. Flip Harden-Runner / network policy to block with registry.npmjs.org deliberately not in the allow list — this is the moment of truth
  6. Watch CI for a week; any failure points to a tool still going direct
  7. Migrate publish workflows to JFrog OIDC (§6.7) and add the hardened publish job (§6.8)
  8. Add consumer-side attestation verification (§6.9) for @mycompany/* packages

6.11 Common pitfalls#

A handful of traps that catch teams setting this up for the first time:

  • Scope mismatch on resolve vs. publish. If .npmrc points @mycompany:registry= at npm-local but publishConfig.registry in package.json points at npm-virtual, publishes go through the virtual's default-deployment routing — usually fine, but if the virtual's default-deployment is misconfigured, publishes silently land in the wrong repo. Pin both sides to npm-local explicitly.
  • Dist-tag confusion. npm publish without --tag defaults to latest. Use --tag beta / --tag next for pre-release branches so a release candidate doesn't become the default install for every internal consumer.
  • Republish protection. JFrog blocks same-version republishes by default. Don't turn this off to "fix" a bad publish — use npm deprecate and a new patch version.
  • .npmrc token leaking into the tarball. A dev's ~/.npmrc gets copied into the build context, then into the published artifact. The files allow-list and the inspection step prevent this — verify on a real npm pack --dry-run before you trust it.
  • Consumer egress not blocked too. Blocking npmjs.org in the publishing pipeline but not in consumer pipelines leaves the back door open. The transitive public dependencies of your internal packages must still resolve through npm-remote — that's the virtual repo doing its job — but a consumer project with its own .npmrc pointing elsewhere bypasses it entirely. Roll the network block to every project, not just publishers.

Conclusion — what has actually changed#

The 2025–2026 wave forced the npm ecosystem to grow up. Trusted Publishing with OIDC, Dependabot cooldown, mandatory WebAuthn for high-impact packages, and provenance attestations are now the supply-chain equivalent of HTTPS — visible defaults, broadly deployed, and worth assuming. The architecturally significant lesson from Mini Shai-Hulud is that provenance is necessary but not sufficient: an attacker who hijacks a maintainer's workflow can still produce valid SLSA-3 attestations, so consumers must pin the workflow path and branch in their verification policies, not just trust the signature.

The asymmetry between attacker and defender has narrowed in a way that surprised many — Aikido caught chalk/debug within minutes, Socket flagged s1ngularity and Shai-Hulud shortly after malicious publishes, StepSecurity caught Shai-Hulud 2.0 in Backstage because the runners contacted endpoints they'd never seen before. Behavioral, AI-augmented SCA is materially better than CVE-based scanning, and the realistic detection window for downstream defenders is now measured in hours, not weeks. That window only closes for organizations that adopt cooldown-based dependency management and registry-layer firewalling.

The novel risk that the s1ngularity incident introduced — abuse of locally authenticated AI assistants — and SANDWORM_MODE's extension into harvesting LLM API keys and poisoning MCP servers signal where the next campaigns will land. Treat AI coding agents as privileged identities. Inventory installed CLIs and extensions. Forbid unattended permission-bypass modes on machines holding production secrets. Monitor outbound traffic to commercial LLM APIs and local LLM endpoints from production and CI hosts. Add LLM API keys to the same rotation cadence as cloud access keys.

The defensive stack that actually works in 2026 is layered and pessimistic: lockfiles, no scripts, signature verification, a scope-pinned private registry, and dev-container sandboxing locally; SHA-pinned actions, OIDC publishing with pinned trust, Harden-Runner egress blocking, cooldown, and SBOMs with attested provenance in CI; distroless images signed by digest, Pod Security restricted, default-deny egress with FQDN allow-listing, Falco/Tetragon runtime, and secrets mounted as files in the cluster; and a kill-the-runner-first incident playbook with a written postmortem expected for every event. None of these are speculative — every control listed has prevented or detected a real attack in the chronology above. The question for senior engineers and security teams is no longer which of these to deploy; it is how fast.

Sources and references#

This article was updated on May 31, 2026 against primary disclosures, vendor research, advisories, and academic work. The links below are the main sources used for the incident chronology and control recommendations.

Incident disclosures and advisories

AI abuse, slopsquatting, and defensive tooling

Share:

Related Articles