Back to all posts
Critical Research

663 uploads in eight days:
a live Shai-Hulud victim, caught on our check's first day

One developer's GitHub account has been publishing the Mini Shai-Hulud worm's stolen-data uploads since 12 October: 663 repositories by 17:41 UTC on 20 October. The hourly check we deployed on 19 October flagged it the next morning. We reported it to GitHub and are not naming it. Here is what the uploads show, what they cannot show, and how to check your own account.

CyberXYZ Security Team Threat Intelligence
7 min read
If your GitHub account has repositories described "Shai-Hulud: Here We Go Again"

A machine or CI job that holds one of your tokens is running the Mini Shai-Hulud worm and uploading what it steals. Run the one-line check below. If it finds anything, clean the machine before you revoke the token: the worm installs a token monitor that deletes your home directory when the stolen token stops working.

Summary

The uploads

What we saw

Between 12 October 14:43 and 20 October 17:41 UTC, one GitHub account gained 663 public repositories. Every one has the description Shai-Hulud: Here We Go Again and a name made of two words from a list of 30 Dune terms plus a number. Each holds a README and a single results/results-<timestamp>-0.json file; the one we listed held 11,221 bytes. The account has one other public repository, which predates all of this. We read only public metadata: names, creation times, sizes and descriptions. We did not open the uploads.

020040060012 Oct14 Oct16 Oct18 Oct20 Octhourly check live 19 Oct 16:56first finding 20 Oct 04:27
fig. 1 · the 663 dead-drop repositories by creation time (UTC), counted up. Blue: every upload, from the account's public repository list. Orange: the 36 that GH Archive recorded, the only ones a check built on that feed can see. Source: GitHub API metadata and GH Archive, read on 20 October.
Data table
Day (UTC)Uploads that dayUploads so farRecorded by GH Archive
Mon 12 Oct660
Tue 13 Oct10210812
Wed 14 Oct1092178
Thu 15 Oct762930
Fri 16 Oct833760
Sat 17 Oct173930
Sun 18 Oct724650
Mon 19 Oct1015665
Tue 20 Oct9766311

The uploads follow a daily rhythm. Most land between 02:00 and 15:00 UTC, which is 10:00 to 23:00 in UTC+8. Overnight they slow to about one an hour without stopping. The quietest day was Saturday 17 October, with 17. A rhythm like this fits a machine that someone uses through the working day better than a job on a fixed schedule, but it is only a pattern; from outside we cannot see the machine.

0003060912151821Mon 12Mon 12 00:00, 0Mon 12 01:00, 0Mon 12 02:00, 0Mon 12 03:00, 0Mon 12 04:00, 0Mon 12 05:00, 0Mon 12 06:00, 0Mon 12 07:00, 0Mon 12 08:00, 0Mon 12 09:00, 0Mon 12 10:00, 0Mon 12 11:00, 0Mon 12 12:00, 0Mon 12 13:00, 0Mon 12 14:00, 4Mon 12 15:00, 0Mon 12 16:00, 0Mon 12 17:00, 0Mon 12 18:00, 0Mon 12 19:00, 0Mon 12 20:00, 1Mon 12 21:00, 0Mon 12 22:00, 0Mon 12 23:00, 1Tue 13Tue 13 00:00, 3Tue 13 01:00, 4Tue 13 02:00, 11Tue 13 03:00, 6Tue 13 04:00, 1Tue 13 05:00, 4Tue 13 06:00, 5Tue 13 07:00, 7Tue 13 08:00, 3Tue 13 09:00, 11Tue 13 10:00, 2Tue 13 11:00, 8Tue 13 12:00, 10Tue 13 13:00, 1Tue 13 14:00, 12Tue 13 15:00, 0Tue 13 16:00, 0Tue 13 17:00, 4Tue 13 18:00, 5Tue 13 19:00, 1Tue 13 20:00, 1Tue 13 21:00, 0Tue 13 22:00, 1Tue 13 23:00, 2Wed 14Wed 14 00:00, 3Wed 14 01:00, 7Wed 14 02:00, 6Wed 14 03:00, 3Wed 14 04:00, 3Wed 14 05:00, 4Wed 14 06:00, 4Wed 14 07:00, 7Wed 14 08:00, 12Wed 14 09:00, 9Wed 14 10:00, 3Wed 14 11:00, 10Wed 14 12:00, 7Wed 14 13:00, 9Wed 14 14:00, 3Wed 14 15:00, 9Wed 14 16:00, 4Wed 14 17:00, 0Wed 14 18:00, 1Wed 14 19:00, 0Wed 14 20:00, 0Wed 14 21:00, 1Wed 14 22:00, 3Wed 14 23:00, 1Thu 15Thu 15 00:00, 1Thu 15 01:00, 3Thu 15 02:00, 8Thu 15 03:00, 5Thu 15 04:00, 0Thu 15 05:00, 0Thu 15 06:00, 7Thu 15 07:00, 8Thu 15 08:00, 2Thu 15 09:00, 11Thu 15 10:00, 5Thu 15 11:00, 3Thu 15 12:00, 10Thu 15 13:00, 3Thu 15 14:00, 2Thu 15 15:00, 0Thu 15 16:00, 1Thu 15 17:00, 0Thu 15 18:00, 0Thu 15 19:00, 1Thu 15 20:00, 2Thu 15 21:00, 0Thu 15 22:00, 1Thu 15 23:00, 3Fri 16Fri 16 00:00, 1Fri 16 01:00, 0Fri 16 02:00, 1Fri 16 03:00, 9Fri 16 04:00, 4Fri 16 05:00, 7Fri 16 06:00, 8Fri 16 07:00, 8Fri 16 08:00, 5Fri 16 09:00, 11Fri 16 10:00, 5Fri 16 11:00, 2Fri 16 12:00, 3Fri 16 13:00, 5Fri 16 14:00, 5Fri 16 15:00, 7Fri 16 16:00, 0Fri 16 17:00, 0Fri 16 18:00, 0Fri 16 19:00, 1Fri 16 20:00, 0Fri 16 21:00, 1Fri 16 22:00, 0Fri 16 23:00, 0Sat 17Sat 17 00:00, 0Sat 17 01:00, 1Sat 17 02:00, 0Sat 17 03:00, 0Sat 17 04:00, 1Sat 17 05:00, 2Sat 17 06:00, 1Sat 17 07:00, 5Sat 17 08:00, 0Sat 17 09:00, 0Sat 17 10:00, 2Sat 17 11:00, 1Sat 17 12:00, 0Sat 17 13:00, 1Sat 17 14:00, 0Sat 17 15:00, 0Sat 17 16:00, 0Sat 17 17:00, 0Sat 17 18:00, 0Sat 17 19:00, 0Sat 17 20:00, 1Sat 17 21:00, 0Sat 17 22:00, 1Sat 17 23:00, 1Sun 18Sun 18 00:00, 1Sun 18 01:00, 0Sun 18 02:00, 0Sun 18 03:00, 0Sun 18 04:00, 1Sun 18 05:00, 9Sun 18 06:00, 0Sun 18 07:00, 2Sun 18 08:00, 1Sun 18 09:00, 0Sun 18 10:00, 11Sun 18 11:00, 9Sun 18 12:00, 2Sun 18 13:00, 2Sun 18 14:00, 8Sun 18 15:00, 6Sun 18 16:00, 2Sun 18 17:00, 7Sun 18 18:00, 0Sun 18 19:00, 4Sun 18 20:00, 0Sun 18 21:00, 3Sun 18 22:00, 2Sun 18 23:00, 2Mon 19Mon 19 00:00, 0Mon 19 01:00, 1Mon 19 02:00, 15Mon 19 03:00, 8Mon 19 04:00, 3Mon 19 05:00, 1Mon 19 06:00, 7Mon 19 07:00, 13Mon 19 08:00, 7Mon 19 09:00, 6Mon 19 10:00, 6Mon 19 11:00, 4Mon 19 12:00, 6Mon 19 13:00, 5Mon 19 14:00, 5Mon 19 15:00, 0Mon 19 16:00, 1Mon 19 17:00, 2Mon 19 18:00, 0Mon 19 19:00, 1Mon 19 20:00, 5Mon 19 21:00, 2Mon 19 22:00, 0Mon 19 23:00, 3Tue 20Tue 20 00:00, 0Tue 20 01:00, 3Tue 20 02:00, 2Tue 20 03:00, 8Tue 20 04:00, 0Tue 20 05:00, 10Tue 20 06:00, 10Tue 20 07:00, 7Tue 20 08:00, 13Tue 20 09:00, 4Tue 20 10:00, 7Tue 20 11:00, 9Tue 20 12:00, 8Tue 20 13:00, 10Tue 20 14:00, 3Tue 20 15:00, 0Tue 20 16:00, 1Tue 20 17:00, 2Tue 20 18:00, 0Tue 20 19:00, 0Tue 20 20:00, 0Tue 20 21:00, 0Tue 20 22:00, 0Tue 20 23:00, 0hour of day (UTC); add 8 hours for UTC+8
fig. 2 · uploads per hour, by day (UTC). Darker means more; the busiest hour had 15. 81% landed between 02:00 and 15:00 UTC (10:00 to 23:00 in UTC+8). Outlined: the hour of the first upload our hourly check saw, 01:45 on 20 October. 12 October starts at 14:43 and 20 October ends at 17:41. Source: GitHub API metadata.
Data table
RowTotalBusiest hour
Mon 12614:00
Tue 1310214:00
Wed 1410908:00
Thu 157609:00
Fri 168309:00
Sat 171707:00
Sun 187210:00
Mon 1910102:00
Tue 209708:00
The channel

How the worm uses your account

Besides its command-and-control server, the worm stages what it collects in new public repositories created with the stolen token, under the token owner's account. StepSecurity documented the Dune-word names and the reversed description niagA oG eW ereH :duluH-iahS in the May AntV wave, and Corgea documented the same marker. In June, StepSecurity described the results/results-{timestamp}.json upload. SafeDep's analysis of the October tensorlake build shows the forward description this account uses. It also shows a new C2 domain and a token monitor that deletes the home directory when the stolen token is revoked; SafeDep found that the monitor arms only for accounts with no organisations.

The October wave is active. The Hacker News reported that the first malicious tensorlake commit landed on 7 October, and that version 0.5.144 reached npm a day later. Nothing we can see ties this account to tensorlake. Our GH Archive data shows the forward description in use since early August.

Limits

What we can and cannot see

Detection

How we caught it, and what we missed first

Our retrospective GH Archive hunt, covering 1 August to 13 October, had already matched 12 of this account's repositories. The first was recorded at 03:05 UTC on 13 October, about 12 hours after the first upload. It was one of the 13 accounts counted in our long-tail post. That was an analysis, not an alert, and we did not act on the list then.

On 19 October at 16:56 UTC the same check went into our hourly GH Archive pipeline. Its first record of an upload after that came at 01:45 UTC on 20 October. The check flagged the account at 04:27 UTC, 2 hours 42 minutes later, with the note "Treat the owning account's tokens as stolen; check packages it can publish." It raised one finding for each of the 9 hours that day in which GH Archive recorded an upload, the last at 16:26 UTC. These findings go to our analysts. They do not alert customers on their own.

The limit is the feed. GH Archive recorded only 36 of the 663 uploads, and none of the 248 made between 15 and 18 October. A check built on it sees a sample of what a victim account does, not all of it.

Mon 12 Oct6 uploads0 in GH ArchiveTue 13 Oct102 uploads12 in GH ArchiveWed 14 Oct109 uploads8 in GH ArchiveThu 15 Oct76 uploads0 in GH ArchiveFri 16 Oct83 uploads0 in GH ArchiveSat 17 Oct17 uploads0 in GH ArchiveSun 18 Oct72 uploads0 in GH ArchiveMon 19 Oct101 uploads5 in GH ArchiveTue 20 Oct97 uploads11 in GH Archive
fig. 3 · uploads per day (blue) and how many of them GH Archive recorded (orange): 36 of 663, and none of the 248 between 15 and 18 October. Source: GitHub API metadata and the hourly GH Archive files for 12 to 20 October.
Data table
Day (UTC)UploadsRecorded by GH Archive
Mon 12 Oct60
Tue 13 Oct10212
Wed 14 Oct1098
Thu 15 Oct760
Fri 16 Oct830
Sat 17 Oct170
Sun 18 Oct720
Mon 19 Oct1015
Tue 20 Oct9711
Check yourself

Check your own account in one command

With the GitHub CLI signed in as the account you want to check, this lists every repository whose description carries one of the worm's markers. No output means none was found.

your account, exact: lists marker repositories, prints nothing if clean
gh repo list "$(gh api user --jq .login)" --limit 2000 --json name,description,createdAt --jq '.[] | select((.description // "") | test("Shai-Hulud|Sha1-Hulud|Hades|Miasma|niagA oG eW"; "i"))'

For an organisation, check its members' personal accounts, since that is where the worm creates the repositories. This loop uses GitHub search, which finds the forward description and depends on the search index. Run the command above for an exact answer on any account it flags.

every member of an organisation: set ORG first
ORG=YOUR_ORG_NAME
gh api --paginate "orgs/$ORG/members" --jq '.[].login' | while read -r u; do
  gh search repos --owner "$u" --match description "Shai-Hulud" --limit 1000 --json fullName,createdAt --jq '.[] | "\(.createdAt)  \(.fullName)"'
done

We tested both. Pointed at this account, the first returned all 663 uploads; pointed at our own, it returned nothing. The second runs in bash and zsh and finds this account's uploads.

For a repository checkout, our CLI's agents scan reads the worm's autorun files without running them. Version 1.4.76 on PyPI reports them as critical and exits 1: xyz code-scan agents . --no-upload. It checks a checkout. It does not look at an account's repositories.

Clean-up

Clean up in this order

Persistence first, tokens second

Revoking the stolen token first can trigger the token monitor, which deletes the home directory. Remove it before you touch any token.

1
Find the machine

Match the upload times against the laptops, servers and CI runners that hold your tokens.

2
Disconnect it

Take it off the network, or stop the runner, so it cannot upload again or receive new commands.

3
Remove the token monitor

~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/ and its systemd user service or macOS LaunchAgent. On Windows, its scheduled task and %LOCALAPPDATA%\gh-token-monitor\monitor.ps1.

4
Remove the rest

The kitty-monitor service and ~/.local/share/kitty/cat.py, plus SessionStart hooks in ~/.claude/settings.json and in any repository you open.

5
Then rotate, from a clean machine

GitHub tokens first, then npm, cloud, SSH and anything else the machine could read. Check every package the account can publish.

6
Delete the dead-drop repositories

They hold your encrypted data. Treat whatever was on the machine as already copied.

on a macOS or Linux machine
ls -la ~/.local/bin/gh-token-monitor.sh ~/.config/gh-token-monitor ~/.local/share/kitty/cat.py 2>/dev/null
ls ~/Library/LaunchAgents 2>/dev/null | grep -E 'gh-token-monitor|kitty-monitor'
systemctl --user list-unit-files 2>/dev/null | grep -E 'gh-token-monitor|kitty-monitor'
grep -n -E 'setup|index.js' ~/.claude/settings.json 2>/dev/null
Indicators

Indicators

These are known public indicators of this worm family, with their sources. None is new to this post.

TypeIndicatorSource
Dead-drop repositories
DescriptionShai-Hulud: Here We Go AgainSafeDep, The Hacker News (October build)
Description, reversedniagA oG eW ereH :duluH-iahSStepSecurity, Corgea (May)
Repository name^[a-z]+-[a-z]+-[0-9]+$Two Dune words and a number (StepSecurity)
Upload fileresults/results-<timestamp>-<n>.jsonStepSecurity (June)
Command and control
Domaint[.]m-kosche[.]comMay and September builds (Corgea)
Domainiseekaigogo[.]comOctober tensorlake build (SafeDep, The Hacker News)
On the infected machine
Token monitor~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/Deletes the home directory on token revocation (SafeDep)
Token monitor, Windows%LOCALAPPDATA%\gh-token-monitor\monitor.ps1SafeDep
Persistence~/.local/share/kitty/cat.py, kitty-monitor serviceCorgea
What we did

What we did

We reported the account to GitHub on 20 October and asked them to disable the dead-drop repositories and tell the owner. We are not naming the account, linking to it, or listing its repositories. Its owner is a victim, and anyone who can read this can find the uploads.

We are not the only ones looking. Since 9 October, an independent researcher has been opening issues on accounts in the same state, such as this one, which has since been closed.

Mapping

MITRE ATT&CK

Credential AccessT1528Steal Application Access Token
ExfiltrationT1567.001Exfiltration to Code Repository
PersistenceT1546Event Triggered Execution
ImpactT1485Data Destruction

References

  1. CyberXYZPublic repository metadata, GH Archive records and hourly-check findings for one victim account (checked 22:33 UTC on 20 October)
  2. CyberXYZThe long tail of Mini Shai-Hulud: 24 more repositories the first reports missed (20 Oct 2026)
  3. StepSecurityShai-Hulud: Here We Go Again. Mass npm Supply Chain Attack Hits the AntV Ecosystem (19 May 2026)
  4. CorgeaMini Shai-Hulud npm worm: the AntV wave (May 2026)
  5. StepSecurityMiasma npm Supply Chain Attack: Self-Spreading Worm via Phantom Gyp (3 Jun 2026)
  6. SafeDeptensorlake 0.5.144 npm Compromise Ships Mini Shai-Hulud Malware (8 Oct 2026)
  7. The Hacker NewsTensorlake npm Package Compromised to Deliver Shai-Hulud Credential-Stealing Worm (8 Oct 2026)
  8. GitHub"Your account is creating Shai-Hulud exfiltration repos", an independent researcher (9 Oct 2026)
  9. MITRET1567.001 Exfiltration to Code Repository
👋
// book a demo

Want to see it block a real attack?

Book 15 minutes. We run real packages through CyberXYZ while you watch, axios 1.14.1 among them, then connect one laptop or one pipeline for a 30-day proof of value.