How I Checked My Mac After the axios npm Compromise
A practical incident-response walkthrough: exact-version scanning, temp-directory false positives, and the guardrails I added for npm and uv.
Today’s axios npm compromise was a good reminder that most supply-chain incidents do not start with sophisticated malware analysis. They start with a simple question: did this actually land on my machine?
In this case, the concern was malicious package versions published on March 31, 2026, including axios@1.14.1, axios@0.30.4, and plain-crypto-js payload versions associated with the incident. I had not run npm that day, but that still leaves room for anxiety because local shells are only one source of installs. CI, Docker builds, automated updates, or background tooling can pull packages too.
So instead of arguing from memory, I ran an exact-match scan and turned the workflow into a reusable Codex skill: axios-compromise-scan.
The first pass: scan the whole machine
The first thing I wanted was a blunt answer. Scan /, not just my active repo, and look for exact matches in:
package.jsonpackage-lock.jsonnpm-shrinkwrap.jsonyarn.lockpnpm-lock.yamlbun.lock- installed
package.jsonfiles undernode_modules
The first full-system scan came back with six exact matches for axios@1.14.1.
That looks bad until you inspect the paths.
All six hits were inside macOS temp storage under a path like:
/private/var/folders/.../T/tmp.XXXXXX
More specifically, they were in two transient directories with small hit/ and clean/ fixture trees. That matters, because /private/var/folders/.../T/ is just the backing location for $TMPDIR on macOS. It is not automatically evidence of a persistent install in one of your projects.
This is the part where incident response can go sideways. If you stop at the first red line in the scanner output, you can convince yourself you are compromised when what you actually found is a temporary test artifact.
The second pass: scan the real persistent scope
The stronger signal was scanning my home directory directly:
python3 scripts/scan_compromised_versions.py --json /Users/gaurav
That scan returned 0 exact matches for the compromised versions under /Users/gaurav.
That result changed the assessment materially. It meant I was not looking at something installed in my normal code, package caches, or home-directory tooling. I was looking at temp noise.
I then removed only the two specific tmp.* directories that contained those fixture files. I did not delete the whole /private/var/folders/... tree, because that would be reckless. Those hashed macOS directories also contain active OS and app temp/cache data.
After deleting only the two identified temp folders, I reran the full / scan. The result was clean: 0 exact matches left on disk.
So was I actually at risk?
My answer after the follow-up scans was: probably not on this Mac.
That is not the same as saying “definitely impossible.” The practical risk model was:
- If I did not run
npm,pnpm,yarn,bun,npx, or a fresh dependency install on March 31, 2026, local exposure was less likely. - If my CI, Docker builds, or automation installed dependencies that day, then my broader environment still needed checking.
- Exact-version scans are useful because they answer the narrow question first: is the known bad package on disk right now?
The operational lesson: add time-based guardrails
The incident also pushed me to put simple delay-based controls in place for future installs.
For npm:
min-release-age=7
For uv:
exclude-newer = "7 days"
These settings do not fix an already-compromised machine. They are a future-facing control. The idea is straightforward: do not eagerly install packages published minutes ago unless you explicitly decide to.
That is not a perfect defense. A malicious package can sit longer than seven days. But it is still a strong operational improvement because many ecosystem compromises get detected quickly, and a short release-age buffer dramatically reduces the blast radius from “freshly published and silently auto-installed.”
I turned the response into a reusable skill
I did not want this to remain a one-off terminal session, so I updated the skill repo to make the workflow repeatable.
The skill now covers two parts:
- Exact-version scanning for the compromised package set and the related filesystem indicators.
- A helper that adds npm and uv release-age guardrails for future installs.
The hardening helper is intentionally conservative. If min-release-age or exclude-newer already exists in your config, it leaves the existing value alone. It only adds the guardrails when those keys are missing.
That small detail matters. Security tooling that “helps” by overwriting somebody else’s existing policy is not helping.
If you want the repo, it is here:
Have a similar problem to solve?
Use the contact page if you want help with agent architecture, evaluation, or implementation.
Get in touch