An npm package installed more than 1 million times a month was quietly stealing credentials from developer machines. Every run, it exfiltrated API keys, environment variables, and auth tokens. If any of your projects pulled it in — directly or through transitive dependencies — your secrets were exposed before you knew anything was wrong.
This is documented. Ars Technica reported it in April 2026. And it won't be the last time. Here's the four-item checklist we're rolling out across every active Tuscan client repo this week.
1. Lockfile audit — and switch to npm ci in CI
If your CI pipeline runs npm install, stop. Swap it to npm ci. The difference matters: npm install rewrites the lockfile silently when there's a version resolution conflict. npm ci fails hard if the lockfile doesn't match package.json. One tells you what it installed; the other just installs whatever it thinks is right.
With npm ci in place, your lockfile is now load-bearing. That means you need to audit it. Run npm audit --audit-level=high as a failing check on every pull request — this catches known CVEs. For supply-chain-specific risk (malicious packages that haven't been flagged in the NVD yet), add Socket Security or Snyk to your PR checks. Socket specifically flags packages that request unusual network permissions, read ~/.ssh, or spawn child processes on install — the exact behaviors a credential stealer exhibits.
Both have free tiers that cover most agency projects. The initial scan takes under five minutes. Run it today on your three most active repos and see what comes back.
2. Block typosquat packages before they install
Typosquatting is still one of the easiest supply-chain attack vectors. Someone types requets instead of request, or lodahs instead of lodash, and installs a package that's been sitting on the npm registry for exactly that moment. At the scale of a team juggling a dozen client repos, this happens.
Two controls close most of the gap:
- Use
--save-exactwhen adding packages.npm install --save-exact reactwrites"react": "18.3.0"instead of"react": "^18.3.0". Exact pins mean your lockfile reflects exactly what you reviewed — no silent range resolution pulling in a different version six weeks later. - Add Socket's GitHub app to client repos. Socket checks every dependency change in pull requests against known typosquat patterns and malicious behavior signals. It blocks the install at review time, not after it's already run in someone's local environment.
Neither control is foolproof. Together they raise the bar enough that most automated supply-chain attacks move to easier targets.
3. Scope CI secrets to the jobs that actually need them
When a malicious package runs in CI, it runs with access to every secret your pipeline injects into that job. If your single build job has AWS_ACCESS_KEY_ID, SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, and ANTHROPIC_API_KEY all in scope at the same time — a credential stealer running in postinstall hits the jackpot in one shot.
The fix is least-privilege secret injection. Separate your CI jobs and inject secrets only at the step level, not the workflow level:
- The "install deps and run tests" job should get no production secrets at all. Mocked environment variables or nothing.
- The "deploy to Vercel" job gets the Vercel token only. That's the only thing Vercel's CLI needs.
- The "run DB migrations" job (ideally a separate workflow, triggered on merge to main) gets the Supabase service role key. Not before, not shared with anything else.
On the Next.js + Supabase + Vercel stack we use across client builds, this split maps cleanly to GitHub Actions jobs: build, deploy, migrate. Three jobs, three separate secret scopes. A compromised package in the build job has nowhere to go.
One more thing: rotate any secret that might have been in scope during a build that installed a compromised package. Don't wait for confirmation. Rotation is cheap; incident response isn't.
4. Add a Version-Sentinel-style install gate
Version Sentinel is an open-source Claude Code plugin that blocks npm install when a dependency is outdated, unrecognized, or flagged by a policy rule. The mechanism is the right model for this problem: intercept the install command before it runs, check the incoming package against a policy, fail fast if something's off.
You don't need the plugin itself to get this behavior. The pattern is portable:
- Add a
preinstallscript topackage.json:"preinstall": "node scripts/check-deps.js" - That script reads the incoming package name (via
npm_config_argv), checks it against an allowlist or calls the Socket API, and exits non-zero if anything raises a flag - In CI, this fires automatically before every
npm ci— no human gate needed
The credential-stealer that reached 1M downloads would have tripped this gate the moment its postinstall hook tried to open a network socket. The install would have failed, CI would have failed, and no secrets would have left the machine.
We're shipping this onto our own internal tooling first to stress-test edge cases before rolling it to client repos. The other three controls go out this week.
The short list for tonight
If you're running a Next.js project on Vercel with a CI pipeline that uses npm install, this is the immediate fix list: swap to npm ci, add npm audit --audit-level=high as a failing check, install Socket's GitHub app, and split your secrets by job. That's under two hours of work and it closes the most obvious attack surface before the next package like this one lands.
The npm ecosystem has a supply-chain problem that isn't going away. The question isn't whether a malicious package will hit your install chain — it's whether your pipeline catches it before it runs.

