How the lockout ended, the entry vector GitHub confirmed, the stealth commits I missed at first, and the cleanup playbook for anyone hit by the same worm.
By Ionut-Cristian Florescu (@icflorescu), written June 8, 2026.
This is the follow-up to Part 1, which I wrote on June 6 while I was still locked out of my account with the Miasma worm's payload live in my repositories. The short version of what changed: it is over, the repositories are clean, and the forensics turned a careful "I can't rule this out" into a confirmed chain of events. Here is the rest of the story.
1. How it ended
I was suspended on the morning of June 4. On June 8 at 07:50 UTC, a GitHub support agent restored the account, forced a password reset, and handed the ticket to a second agent for the technical questions. That was, in total, roughly four days of being locked out of a sixteen-year-old account during an active incident I had reported myself.
I won't pretend I know exactly what moved it. What I can say is that the resolution came after the story went public, after the write-up, the Hacker News thread, and security researchers like Adnan Khan calling it out. Whether that mattered or it was simply my place in the queue, I can't prove. But the lesson I draw is the same one Part 1 ended on: a verified, evidence-complete, actively-dangerous case should not need to go viral to get worked.
Once I was back in, removing the payload took an afternoon. The how is in section 5.
2. The entry vector, now confirmed
This is the part I could only theorize about in Part 1, and the part GitHub's logs settled.
The commits were made with my GitHub CLI (gh) OAuth token, created on 2026-01-17, used from IP 52.240.186.x, a Microsoft Azure range, via the GitHub API (the support team found API events only, no Git operations). Three things fall out of that:
- It was a stolen credential of mine, not a breach of my password or 2FA. Exactly as Part 1 argued: 2FA never gated this, because token and API writes don't go through interactive login.
-
It explains the forged, unsigned commits. The commits were created through the API, where the caller sets the author field freely and nothing is server-signed. That is why they show up as
github-actions(or, on side branches, as me) and asverified: false. My Part 1 phrasing of "a plaingit push" was the one technical detail I got slightly wrong: it was the API, not the git protocol. The conclusion, a forged author on an unsigned commit, was right. -
It confirms the credential theft I feared in Part 1, with a twist I did not expect. That
ghtoken traces back to January, before I rebuilt both machines from scratch in mid-May. I assumed, and said in Part 1, that it had simply sat untouched since then. The logs say otherwise: I was actively rotating my GitHub CLI tokens that month. The catch is structural, and it is the real lesson here. GitHub stacks many access tokens under a single OAuth app authorization, one per machine and per re-login, accumulated over months. Revoking an individual token, re-authenticating, even GitHub's own automatic eviction of old tokens once you hit the per-app cap, none of it revokes the grant. Only revoking the whole authorization kills every token at once. My cleanup rotated and destroyed individual tokens but never closed the authorization, so the one token the attacker had harvested survived underneath it.
I also recall trying to revoke the entire GitHub CLI authorization through the web UI during that same stretch, when GitHub was visibly unstable, and not trusting that it went through. I cannot tie that to a specific audit entry, and a failed action may not leave one, so I will put it no stronger than a recollection. When I asked, GitHub gave me that credential's complete lifecycle, and it is short: created on January 17, regenerated three seconds later inside the same gh login, and then nothing of mine at all, until a GitHub staff action destroyed it on June 4, when my account was locked and its password randomized. Not one revocation by me in between. So the part that matters is now documented rather than inferred: the authorization was never revoked, the dangerous token lived under it for nearly five months until the suspension finally killed it, and I had been actively trying to clean these credentials up rather than ignoring them. In Part 1 I couldn't rule out a credential stolen before the rebuild that outlived it; that is confirmed, and the only surprise is in how it outlived it. If there is one practical takeaway from this whole incident, it is the smallest and least-known one: to cut off a tool like the GitHub CLI, you revoke its authorization, not its tokens.
And that leaves a question I cannot answer, and neither, as far as I can tell, can GitHub's interface: during the stretches when the platform is erroring, how is anyone supposed to confirm that a revocation actually took effect? A revoke that returns a unicorn instead of a confirmation is indistinguishable, from the user's side, from one that succeeded. You click, you get an error page, and you genuinely do not know whether the credential is now dead or still live. For an ordinary setting, that is an annoyance. For a security action, on a day the platform is under attack, that ambiguity is its own kind of failure.
Where the token was harvested in the first place is the one link still inferred rather than proven. The strongest candidate, raised by security researcher Daniel Ruf and consistent with the campaign's documented method, is a compromised dependency in the TanStack supply-chain wave. I run a project on TanStack Start, the timing fits, and the same worm reused a Microsoft contributor's credential that traced back to a compromise a month earlier. I can't put a specific npm install to it, and I won't pretend to.
3. The stealth I missed at first
In Part 1 I described five repositories, each hit once on main with a commit forged to look like the github-actions bot. That was only half of it.
When I came back and actually scanned, I found that every repository was hit on a second branch too, with a far sneakier disguise. The side-branch commits were forged under my own name, given plausible messages ("Update deps", "Bump version", "Update V6 contributors"), and backdated so they read as old, routine work on a quiet branch:
| Repo (side branch) | Forged author | Backdated to |
|---|---|---|
| mantine-datatable / next | me | 2026-05-20 |
| mantine-contextmenu / next | me | 2026-05-14 |
| next-server-actions-parallel / next | me | 2025-01-09 |
| mantine-datatable-v6 / next | me | 2024-01-17 |
| mantine-contextmenu-v6 / next | me | 2023-11-10 |
Look at the dates. They get progressively older, and the messages get tailored per repo. Whoever ran this took the trouble to make each stealth commit blend into that specific project's history. If I had trusted the obvious github-actions wave and stopped there, these would still be sitting in my repos, dated like commits I made years ago.
The practical lesson is blunt: do not detect these by author. A backdated commit forged under the maintainer's own name passes every "is this the bot?" check. Detect by the payload file instead. The reliable test across all branches is whether .github/setup.js exists:
git fetch origin
git for-each-ref --format='%(refname:short)' refs/remotes/origin | while read b; do
git cat-file -e "$b:.github/setup.js" 2>/dev/null && echo "INFECTED: $b"
done
4. The blast radius was wider than five repos
Part 1 was about icflorescu. The same payload, dated inside the same 49-second June 3 window, also landed in a repository under a separate organization account I control, again on both branches, again with the two-disguise pattern. I am still sweeping the rest of my accounts and will update if more turns up. The takeaway for anyone reading: if one of your accounts was hit, assume the worm walked every repository every harvested token could reach, and scan all of them.
5. The cleanup playbook
If you are cleaning up the same thing, here is what worked, and the one rule that matters: never let an affected repo execute. No opening it in an AI editor, no npm install, no npm test. A no-checkout clone keeps the payload off your disk entirely.
# Scan every branch for the payload (see section 3).
# Evidence backup once, as a bare clone (no working tree, nothing runs):
git clone --mirror https://github.com/<you>/<repo>.git evidence.git
tar czf evidence.tar.gz evidence.git
# Work clone with no checkout:
git clone --no-checkout https://github.com/<you>/<repo>.git fix
cd fix
# For each infected branch, confirm the malicious commit is the tip, then
# reset the branch to its parent (drops the commit out of history):
git push --force-with-lease origin <MALICIOUS_SHA>^:refs/heads/<branch>
Two deliberate choices. First, I reset rather than git revert: a revert leaves the 4.3 MB dropper retrievable at the old commit SHA, and for live malware you want it gone, not archived. The attack is preserved where it belongs, in this write-up and in GitHub's logs, not as a live blob in my history. Second, the reset alone is not the end: because of the fork network a commit can stay reachable by SHA after it is off every branch, so I gave GitHub Support the full list of malicious SHAs and asked them to garbage-collect and purge them via the sensitive-data removal process. That is the definitive kill.
And the reassurance that has not changed since Part 1: the npm packages were never touched. No malicious version was ever published. Everything above is about the GitHub source repositories. If you install my packages from npm, none of this ever reached you.
6. Credit where it's due, and the gap that remains
I was hard on GitHub in Part 1, and I stand by every factual word of it. But fairness cuts both ways, so: the support engineers who eventually worked this, especially the one who pulled the API logs, were excellent. The forensics they provided, the token identity, the source IP, the API-versus-git distinction, are exactly what let me write this with confidence instead of speculation. When a human finally engaged, the engagement was good.
The gap was never the people. It was the days of silence before one of them got to it, and the automated suspension that locked the victim out of his own incident while leaving the weapon live. Both of those are process, not personnel, and process is what I hope GitHub looks at. A verified owner reporting an active payload in his own public repos should be a fast lane, not a four-day queue.
7. What I'm changing
- Long-lived classic tokens are gone. Fine-grained, least-scope, short-expiry tokens only, and I revoke anything I am not actively using.
- Required signed commits and branch protection on the repos, so an unsigned forged push like these cannot land on a protected branch in the first place.
- GitHub Actions pinned to full commit SHAs,
GITHUB_TOKENscoped to read-only by default. - A standing habit of scanning my own repos by payload file, not by author, after any ecosystem-wide supply-chain wave.
8. A warning to other maintainers, and the questions I'm left with
If you take one operational lesson from my four days, take this: your account can be taken from you by an automated system, at the worst possible moment, with no human in the loop and no fast way back. I was not suspended for anything I did. An automated process saw anomalous activity on my repositories and locked the owner out, which meant the one person most motivated to pull the payload down, me, was the one person who could not. If your livelihood runs through a single platform account, that is a single point of failure you do not control. Mirror your repositories somewhere independent (I now push to Codeberg and GitLab alongside GitHub), keep local clones current, and do not assume that having done nothing wrong will keep you logged in.
That leads to questions I don't think are mine alone to answer, so I will pose them rather than pretend to settle them.
At GitHub's scale, has automation been forced to act faster than humans can judge, with the cost being a system that sometimes punishes the victim in order to contain the threat? When an automated mitigation locks a maintainer out of his own live incident, is it containing the spread, or quietly adding to it, by removing the one person who could revert the payload while leaving the payload up? And the newer question: this attack weaponized AI coding agents as its execution vector, and it unfolded during an industry-wide rush to put AI into every developer tool, faster than any of it is being secured. Are we shipping attack surface faster than we can defend it?
I don't have clean answers. I have one data point, my own, and a strong suspicion that I am not the last person this will happen to. These deserve more than a closing paragraph, so I will come back to them properly in a separate piece. For now I will only say that an ecosystem this important should be able to tell a victim from an attacker in less than four days.
9. Closing
Part 1 was written by someone in the middle of it, locked out and angry, and I left it unedited on purpose. Part 2 is written by someone on the other side, with the logs in hand. The arc I take from the whole thing is simple: I did the careful things and still got hit, because a token I created in January outlived the machine I created it on. The attacker was patient and meticulous, down to backdating commits by years. GitHub's automation made the victim's bad day much worse, and its people, once reached, made it right.
If you maintain open source, the one habit worth taking from this is the smallest one: treat the config files of your AI editor as executable code, and check your branches by what they contain, not by whose name is on the commit.
Ionut-Cristian Florescu, June 8, 2026.
Part 1, the original in-the-moment account, is at https://dev.to/icflorescu/the-bot-that-never-was-2mfp (source: https://codeberg.org/icflorescu/miasma-github-incident). The forensics in section 2 are from GitHub Support's response on ticket #4448974; everything about my own repositories is reproducible from the public commit metadata and the decrypted payload.
Top comments (2)
The five-month gap between token creation (January) and abuse (June) is what makes this so hard to catch. Most IR playbooks say "check for suspicious recent activity" but the initial credential theft happened months before any malicious action — there's nothing anomalous to find until the attacker decides to use it. And the "wipe and reinstall = clean slate" assumption is broken for basically every cloud credential now. Your
ghauth,aws configure,gcloud auth, npm publish tokens — they all live server-side and survive any local operation. The January token didn't care that you reinstalled in May because GitHub never knew the machine changed. That's a mental model problem more than a technical one, and I'd bet most developers still think a fresh OS means a fresh start.This is the sharpest summary of the whole thing I've read, thank you, @circuit. Both halves are exactly right, and the dormancy one is the part that still unsettles me: there was genuinely nothing anomalous to find for five months, because a harvested token sitting idle looks identical to one that was never stolen. The malicious activity is the first signal, and by then it has already happened.
On the "fresh OS, fresh start" point, I'd add a second failure mode underneath yours, because it caught me even though I wasn't being passive about it. I wasn't ignoring these credentials. The logs show I was actively rotating my
ghtokens in that window, and I have a clear recollection of trying to revoke the GitHub CLI authorization through the web UI around then, on a day GitHub was visibly erroring. The problem is that GitHub stacks many tokens under a single OAuth app authorization, one per machine and per re-login. Rotating or destroying individual tokens, or even GitHub auto-evicting old ones when you hit the cap, never touches the grant. Only revoking the whole authorization kills every token under it. So the mental-model bug isn't just "reinstalling doesn't revoke". It's that even deliberately cleaning up your tokens doesn't revoke the thing that issues them, unless you knew to go one level up. I thought I'd cut it off. I'd been pruning leaves off a branch that was still attached.And the part I genuinely can't close: when the revoke UI is erroring, you have no way to confirm whether the action took. A unicorn page is indistinguishable from success. For a security action on a bad day, that ambiguity is its own failure.
You've put this more clearly than I managed in the post itself, so I'm going to fold the distinction in and clarify the writeup a bit. Thanks for sharpening it.