october 2026 · 4 min

Contain Before You Revoke

The npm worm behind the 42 armed GitHub repos that JFrog wrote up this week ships with a dead man's switch. It steals your GitHub token, then installs a small service that keeps asking GitHub whether that token still works. The moment the answer is no, it deletes your home directory. Your incident response runbook says "revoke the token" in step one. Here, step one is the trigger.

The Mechanism

SafeDep's analysis spells it out. A persistence service called net-health-checker "checks the stolen GitHub token every 45 seconds for 48 hours". On a 401 or 403 it runs rm -rf ~/. It's installed as a systemd user service on Linux and a LaunchAgent on macOS, so it survives a reboot and doesn't need root.

The worm doesn't need to detect your response team. It only needs to notice the one action every response team takes first. A 401 from GitHub is the signal, and you are the one who sends it.

This isn't a one-off trick either. Akamai documented the same pattern in Mini Shai-Hulud back in May: a monitor polling every 60 seconds, wired to rm -rf ~/. The code for that one was released publicly, so expect to see it in more strains. When the attacker holds the credential, revocation stops being a cleanup step and becomes something they can watch for.

Order of Operations

Both vendors say to remove persistence before you revoke, which is the opposite of the reflex. JFrog says to remove the net-health-checker and pkg-runtime persistence "before revoking the stolen GitHub token". SafeDep's sequence:

  1. Isolate. Take the host off the network. The switch fires on a 401 or 403, and a machine that can't reach GitHub can't get one.
  2. Remove persistence. Kill the services, then delete their files.
  3. Rotate. All of it: Git hosts, registries, cloud, Vault, Kubernetes, SSH, AI provider keys. Do it from a clean machine.
  4. Reinstall. SafeDep's last step is a fresh OS. Don't try to clean the host by hand.

Step 2 on an infected Linux box, from SafeDep's remediation:

systemctl --user disable --now net-health-checker pkg-runtime

And on macOS, using the LaunchAgent paths from the same report:

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.net-health-checker.plist
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.pkg-runtime.plist

Those are this strain's names. The next strain will pick different ones, so the habit that carries over is looking at what runs as the user, not only what runs as root: systemctl --user list-units --type=service and ls ~/Library/LaunchAgents. User-level services need no root and are easy to miss when you only look at system-level ones.

The Tradeoff

Containing first means the stolen token stays live a little longer, and the attacker already has a copy of it. That cost is real, and it's why you do containment in minutes, not hours. Revoking first does end their access a few minutes sooner. Weigh that against a wiped home directory, on a machine whose disk you also need for the investigation.

The harder case is scale. If the token is an org-wide credential, or you rotate a shared secret for every developer at once, a single revocation trips the switch on every infected laptop at the same moment. Find the infected hosts first (JFrog and SafeDep both publish the file paths), isolate them, then rotate. The checker gives up after 48 hours, but you rarely know when its clock started, so don't plan around waiting it out.

I wrote last week that you can only rotate the secrets you can find. This is the second half: rotation has an order now, and the malware is built around the usual one. A runbook written once and never questioned is exactly the lack of agency these campaigns rely on. Go reread yours, and if "revoke" comes before "isolate", swap them.

← all posts