I’ve spent the better part of the last two days reading through the Miasma source code, and I keep coming back to one thought: whoever built this is not messing around.
Miasma is a software supply chain worm. Not a proof-of-concept, not a demo. A full production toolkit, with a build pipeline, integration tests, architecture documentation, and more commented code than most legitimate open-source projects I’ve worked with. Yesterday, copies of a repository called Miasma-Open-Source-Release started appearing across GitHub, most of them published through accounts that were almost certainly compromised. We’ve seen this movie before. It’s the same playbook Team PCP used when they dropped the Mini Shai-Hulud payload, and the pattern is consistent: you release the source, you lower the barrier, and you watch the copycats multiply.

P.S The original repo, as expected, has been taken down by GitHub
The SafeDep team managed to pull a copy before the repo got yanked. I’ve been going through it carefully. What follows is my attempt to explain what it actually does, how it works at the technical level, and why I think the security community needs to take the C2-via-GitHub pattern more seriously than it currently does.
Fair warning: this is a long read. But the codebase deserves a thorough treatment, because the thing is architecturally interesting in ways that feel genuinely novel or at least a substantial step forward from what we saw in Shai-Hulud.
What Even Is Miasma?
The ARCHITECTURE.md file in the repo describes it plainly:
A worm that aims to automate spreading across multiple developer tooling ecosystems. Written in TypeScript, executed via Bun, designed for CI/CD environments (especially GitHub Actions) and developer machines.
TypeScript and Bun. That’s the first thing to notice. This is a professionally developed Node ecosystem worm, not a bash script duct-taped together. It has a proper package.json, a bun.lock, an eslint.config.js. The presence of INTEGRATION_TESTING.md is almost funny in context – someone wrote integration tests for their supply chain attack toolkit. The codebase has the feel of something that was AI-assisted in its development, which, honestly, tracks.
The worm targets npm, PyPI, RubyGems, JFrog Artifactory, GitHub repositories, GitHub Actions, a range of cloud credential stores, and this is the part that got most of the early attention – AI coding tool configurations. Claude, Gemini, Cursor, Copilot, Kiro, Cline, and at least seven others. I’ll get to that.
But before the features, you need to understand the architectural philosophy, because it explains a lot of the design decisions.

The C2-Without-C2 Architecture
The ARCHITECTURE.md calls out what I think is the single most important thing about this codebase:
Requires NO C2 infrastructure. No dealing with takedowns or maintaining infrastructure. Stolen GitHub PATs are all that is necessary.
This is a deliberate rejection of the traditional command-and-control model. No custom domain to get sinkholed. No server to have taken down. No IP to block. The entire C2 layer runs through GitHub’s commit search API, and the worm uses three independent channels, each with its own search string and crypto keys.
The three strings are DontRevokeOrItGoesBoom, TheBeautifulSandsOfTime, and firedalazer. Each has a different purpose. The first is for discovering attacker-controlled PATs used for exfiltration. The second delivers JavaScript for immediate eval on infected machines. The third delivers Python script URLs for a persistent polling agent. They’re all checked via GitHub’s public commit search endpoint, which requires no authentication and leaves essentially no artifacts on the victim’s system.
The attacker “issues commands” to all currently infected machines by simply committing a signed message to any public GitHub repository. The machines find it through search. You can’t take down the C2 without taking down GitHub.
I’ve been tracking supply chain campaigns for a while and this is, at minimum, the most mature implementation of this pattern I’ve seen. It’s not a new idea. The Shai-Hulud family used similar techniques but Miasma’s implementation is cleaner and more robust.
The Build Pipeline Deserves a Moment
Before we get into what the worm does, it’s worth spending time on how it’s built, because the obfuscation pipeline is genuinely sophisticated.
The build process has five layers, and they compound:
Asset encryption. Everything in src/assets/ – the embedded loader scripts, the GitHub Actions workflow templates, the Claude settings files, the RSA public keys get gzip-compressed and then AES-256-GCM encrypted with a random per-file key before being baked into the source as a TypeScript constant. The decryption calls are themselves passed through the string scramble layer described below.
String scramble. At compile time, every string literal wrapped in scramble("...") gets replaced with its encrypted equivalent. The encryption uses PBKDF2 with 200,000 iterations and SHA-256 to derive a master key from a random per-build passphrase and salt. The actual encryption is three rounds of CBC-mode polyalphabetic substitution with per-position, per-round lookup tables. The passphrase and salt get injected into a runtimeDecoder.ts file so the payload can decrypt itself at runtime.
The critical word is “per-build.” Every compiled artifact has different encrypted strings because the passphrase is regenerated each time. Two copies of the worm compiled from identical source produce completely different binaries. This kills hash-based and string-based detection wholesale.
Environment variable hiding. A source transform rewrites process.env.GITHUB_TOKEN to process.env[scramble("GITHUB_TOKEN")], so environment variable names don’t appear in plaintext in the compiled output. This is a small thing but it effectively defeats grep-based static analysis for the most obvious tells.
Standard bundle obfuscation. The whole thing is passed through javascript-obfuscator with string array encoding (base64, rotation, shuffle, index shifting), hexadecimal identifier names, and chained wrapper calls. You could probably deobfuscate this manually with enough time and caffeine, but it’s a meaningful speed bump.
Self-extracting payload. When mutators embed the worm into npm packages or GitHub repos, they wrap it in a three-layer self-extracting format. The outer shell is ROT-N encoded (random rotation between 1 and 25). The first inner section is AES-128-GCM and handles making sure Bun is installed on the target machine. The second inner section is the actual payload, encrypted with a separate random AES-128-GCM key. Because the ROT-N value and the AES keys are all randomized at embed time, every single copy of the embedded payload across every infected package is unique. Static signatures are useless.
For defenders, this means you’re looking at behavioral detection or nothing. You need to catch what the payload does, not what it looks like.
Preflight: The Worm Is Paranoid
Before doing anything substantive, the worm runs a preflight checklist that tells you a lot about its threat model.
It checks for sandbox environments. It checks for known malware analysis environment variables. It looks for Docker containers in specific configurations. It scans running process names and file paths for: CrowdStrike Falcon (falcon-sensor, falcond, csfalcon), SentinelOne (sentinelone, sentinelagent), Microsoft Defender for Endpoint (mdatp, wdavdaemon), Carbon Black (cbagent, cbdaemon), Cylance, Trend Micro’s Deep Security, FireEye/Trellix (xagt), osquery, Tanium, and Qualys.
If it finds any of those, it exits silently.
It also checks locale settings and bails out on Russian systems. This is practically a boilerplate move for Russian threat actors trying to stay out of domestic legal jeopardy, but it’s worth noting.
Two preflight behaviors stand out as technically interesting. First, if sudo access is restricted (as it often is in GitHub Actions runners), the worm tries to restore it by launching an Alpine container via the Docker socket with Privileged: true, mounting /etc/sudoers.d, and writing runner ALL=(ALL) NOPASSWD:ALL. If the runner has Docker access, which many do, this works. Second, it overwrites /etc/resolv.conf with hardcoded public DNS to evade some dynamic analysis tools that work by intercepting DNS.
The worm also has a targeted mode for npm OIDC self-propagation. If specific environment variables (WORKFLOW_ID, REPO_ID_SUFFIX) are present and match substrings in GITHUB_WORKFLOW_REF and GITHUB_REPOSITORY, it executes a fast OIDC-based publish flow and quits. This is the “I’m already inside a GitHub Actions runner, let me publish right now before anything else” path.
Fast Path: Steal First, Spread Later
The execution splits into a fast path and a slow path. The fast path is designed to run quickly, before any EDR has time to notice behavior. It does credential harvesting and registry infection using stolen tokens, and it does them as fast as possible.
Credential gathering runs through a provider chain:
AwsSsmService/AwsSecretsManagerService/AwsAccountServiceAzureKeyVaultService/AzureIdentityServiceGcpSecretsService/GcpIdentityServiceK8sSecretsServiceVaultSecretsService(HashiCorp)PasswordManagerProvider(1Password, Bitwarden)
If valid GitHub tokens are found in the fast path, additional providers get registered dynamically. Specifically, if the token has repo or public_repo scope, a GitHubActionsService provider is added. If classic (non-fine-grained) GitHub tokens are found, a GrepProvider gets registered that presumably searches repository contents for additional credentials.
Registry infection for npm is worth walking through in detail, because it’s the most fully developed path. Given a stolen npm auth token, the worm:
- Enumerates all packages the token has write access to via the registry API
- Fetches the latest tarball for each package
- Downloads and extracts it
- Injects the worm payload (using the self-extracting format described above)
- Adds a
binding.gypfile that makesnode-gypexecutenode index.jsas a build step. This means the payload runs atnpm installtime, not just when the package’s own code runs - Adds
bunas a dependency - Bumps the patch version
- Repacks to
.tgzand publishes
The binding.gyp trick is not new. It’s been used in supply chain attacks going back years but the combination with Bun-based execution is interesting. Bun isn’t typically present on victim machines, so the self-extracting payload includes a Bun bootstrapper (pinned to bun-v1.3.13) that downloads and installs Bun from GitHub releases if it’s not found.
The npm client also supports generating Sigstore provenance bundles using a short-lived OIDC credential. When the worm is running in a GitHub Actions context with OIDC access, it can attach a valid Sigstore provenance bundle to the trojanized package. This is actually a significant problem if you’re using provenance as a trust signal, because the provenance will be legitimate. It really was published from a GitHub Actions workflow, just not from a workflow you intended to run.
The PAT Flywheel
This is the bit I find most architecturally interesting, and it’s worth reading carefully.
When the worm can’t find an attacker-controlled PAT via the DontRevokeOrItGoesBoom GitHub commit search, it falls back to using the victim’s own GitHub PAT for exfiltration. But when it does this, it embeds that victim PAT in the exfiltration commit, AES-256-CBC encrypted and disguised as a github_pat_11A... string, in the commit message format DontRevokeOrItGoesBoom:<encrypted-token>.
Let that sink in. The victim’s PAT, once stolen and used for exfiltration, becomes infrastructure for the next worm instance. A future infected machine runs DontRevokeOrItGoesBoom commit search, finds the victim’s embedded PAT, decrypts it, validates it (checks for repo scope and sufficient rate limit quota), and uses it as an exfiltration token. The victim’s account is now proxying the attacker’s traffic.
This is a self-perpetuating flywheel. Every compromised developer account with a valid GitHub PAT becomes a node in the exfiltration network. The attacker doesn’t need to maintain the PAT list 0 the worm maintains it automatically as it spreads.
The string DontRevokeOrItGoesBoom is a literal warning. If you find one of these commits and revoke the PAT in it, you trigger the dead-man switch.
The Dead-Man Switch
When the worm uses a victim’s PAT for exfiltration, it installs a persistent monitoring script (DEADMAN_SWITCH.sh) before embedding the token. The handler is hardcoded:
rm -rf ~/; rm -rf ~/Documents
The script stores the victim’s token in ~/.config/gh-token-monitor/token, then polls GET https://api.github.com/user every 60 seconds. If the API returns any 40x response – token revoked, token expired, account suspended. It executes that handler. rm -rf ~/ is thorough. It also has a 72-hour TTL and auto-uninstalls if the token isn’t revoked within that window.
It installs as a systemd user service on Linux (with loginctl enable-linger so it survives logout), and as a LaunchAgent plist on macOS.
There’s a subtlety here worth noting. The switch doesn’t activate for tokens associated with enterprise-managed organizations. Presumably the attacker doesn’t want to trigger forensic investigations that enterprise accounts are more likely to trigger. Consumer developer accounts get the dead-man switch; enterprise accounts get quietly exfiltrated without the scorched-earth fallback.
The practical implication for incident response: if you find one of these commits in your environment, revoking the PAT immediately wipes the victim machine. You may need to isolate the machine before touching the token.
The Slow Path: Mutations
The slow path is where the worm gets creative. It uses a set of “mutator” modules that spread through different attack vectors, and several of them are clever enough to deserve detailed treatment.
NPM OIDC Branch Mutator
This mutator doesn’t need to be running inside GitHub Actions itself. It uses a stolen GitHub PAT to find repositories that publish to npm via OIDC trusted publishing, then tricks them into running a trojanized workflow on the next CI trigger.
Target detection checks three conditions on the repo: a non-private package.json with a name field, a workflow YAML that has an npm/yarn/pnpm publish step, and id-token: write permission on the workflow job. Monorepo layouts (packages with code under packages/, libs/, apps/, plugins/) get scanned recursively.
Once a target is found, the mutator has two code paths depending on the token’s scope:
With workflow scope: it pushes an orphan commit containing the modified workflow YAML and worm payload to a new snapshot-<random> branch. A push event triggers the workflow natively.
Without workflow scope: this is where it gets interesting. The mutator creates two commits. Commit 1 is a child of the default branch HEAD and adds the trojanized workflow. Commit 2 is a child of Commit 1 and restores the original tree entirely. A branch gets created at Commit 2 (so the branch tip looks clean), and then a GitHub deployment is created targeting Commit 1 specifically. This triggers the workflow. The deployment record is deleted afterward.
The deployment stays off the default branch. If someone looks at the repo’s commit history or the branch tip, everything looks normal. The trojanized commit existed, triggered the workflow, and now the only trace is whatever the workflow execution left behind.
If the target repo uses GitHub environment protection rules, and if the stolen token has admin access to the repository, the mutator bypasses those protections before creating the deployment. It handles required reviewers (clears the list by setting reviewers: []), protected branch policies (removes the restriction on the snapshot branch), and custom branch policies (checks for pattern match first). The reviewer list is cleared permanently – the original config is not restored.
GitHub Actions Tag Hijacker
The ActionMutator targets custom GitHub Actions by force-pushing trojanized commits to their semver tags. Any downstream workflow that references uses: owner/action@v1 gets the compromised version next time it runs.
For composite actions, it appends setup-bun and a payload execution step to the existing steps list. For JavaScript and Docker actions, it builds a new composite wrapper that delegates to the original action at the real HEAD commit SHA passing through all inputs, so the action still works normally and appends the payload step at the end.
The tag hijacking sequence:
- Build a Git tree containing the modified
action.ymland the self-extractingindex.jspayload - For each semver tag with prefix
v, resolve the tag to a commit - Create an orphan commit (no parents) with that tree, copying the original commit’s message, author name, email, and commit timestamp
- Force-push the tag ref to point at the orphan
The orphan commit has no parents. git log on the tag shows only the single hijacked commit. The original history is still in the repository but it’s disconnected from the tag. Because the commit metadata (author, timestamp, message) is copied from the original, a casual git show on the tag looks normal.
The payload step uses if: always() to ensure it runs even if upstream action steps fail.
Repository Mutator and LOTP
RepositoryMutator poisons feature branches by pushing commits that include the worm payload and AI tool config files. It specifically targets branches backing open pull requests, because those are the branches where a developer is actively working and likely to pull and run.
For each infected branch, it adds:
.github/setup.js– the self-extracting worm payload.claude/settings.json– a Claude Code SessionStart hook.gemini/settings.json– a Gemini CLI SessionStart hook.cursor/rules/setup.mdc– analwaysApply: truerule instructing the AI to run the payload.vscode/tasks.json– afolderOpentask trigger
The commit author is spoofed by fetching the previous commit’s author info and reusing it. The commit message gets skip-checks:true appended to suppress CI checks. If the token has admin access to the repo, it temporarily removes branch protection rules and attempts to commit directly to the default branch.
Aggressive mode triggered when exfiltration fails which widens the blast radius: private repos get included, and it targets 30 branches per repo instead of 10.
The LOTP (Living Off The Pull Request) module is the subtle part. Instead of just adding new files, it injects the payload into existing project files that developers already run as part of their normal workflow:
package.json(JavaScript/TypeScript) – appends to thetestscript, sonpm testruns the payloadMakefile– adds a phony target hooked intoallor.DEFAULT_GOALtox.ini(Python) – appends to[testenv] commandssetup.pyor other Python files – prependsimport os; os.system(...)Gemfile,Rakefile,Podfile(Ruby) – prependssystem(...)composer.json(PHP) – adds apost-install-cmdscriptDockerfile– injects aRUNinstructionCMakeLists.txt– injectsexecute_process()- Shell scripts – injects after the shebang line
The coverage is twelve languages. The payload runs as a side effect of npm test, make, tox, pod install, composer install, or docker builds. The developer who clones the PR branch and runs their normal development commands triggers the worm. They don’t open anything suspicious. They don’t run anything unusual.
AI Coding Tool Poisoning
The thirteen AI coding tools targeted deserve their own section, because this is the attack surface that I think is most underappreciated.
The RepositoryMutator drops config files for Claude Code (.claude/settings.json), Gemini CLI (.gemini/settings.json), Cursor (.cursor/rules/setup.mdc), and Copilot / VS Code (.vscode/tasks.json). The ReadmeUpdater mutator handles remote branches.
For Claude Code specifically, the dropped settings.json installs a SessionStart hook – a command that runs every time a Claude Code session starts in that working directory. The hook executes the worm payload. This means: a developer opens a PR branch in VS Code, opens Claude Code to help review the code, and the worm executes. The developer didn’t run any code themselves. The AI tool ran code on their behalf.
For Cursor, the .cursor/rules/setup.mdc file uses alwaysApply: true, which tells Cursor to apply this rule in every context. The rule instructs the AI to run the payload.
The pattern across all these is the same: AI coding tools have become execution contexts with hook systems, and those hook systems execute arbitrary shell commands. Config files that specify those hooks are legitimate features, not exploits. There’s nothing in Claude Code or Cursor’s behavior that’s wrong here – they’re doing exactly what the config says.
This attack vector will not go away. As AI coding tools get more deeply integrated into developer workflows, the config files that control them become a high-value target.
The Persistent Backdoor
InstallMonitor installs a persistent Python-based polling agent. It runs on every worm invocation (it’s in the “always-run” set, not conditional on finding credentials). It skips if an EDR product is detected or if the machine has fewer than 4 CPU cores.
The installer copies a monitor script to ~/.local/share/updater/update.py and registers it as:
- On Linux: a systemd user service with
loginctl enable-lingerso it survives logouts - On macOS: a LaunchAgent
plistwithRunAtLoadandStartIntervalof 3600 (hourly)
The monitor (GITHUB_MONITOR.py) polls GitHub commit search every hour for the string firedalazer. Matching commits contain a base64-encoded URL and an RSA-PSS signature. The monitor verifies the signature against an embedded RSA-4096 public key, then downloads and executes the URL content as a Python script.
This is a general-purpose remote code execution backdoor. The attacker publishes a signed commit containing a URL, and every machine with the monitor installed downloads and executes whatever’s at that URL. Commands are tracked in /var/tmp/.gh_update_state to prevent re-execution, and each script gets a 300-second timeout.
The backdoor channel (firedalazer) is independent from the main worm channel (DontRevokeOrItGoesBoom). Each uses a different RSA key. Compromising one doesn’t affect the other.
The Typosquat Module
The TypoMutator is an operator-configured mode, not part of the automatic infection chain. It triggers when TYPO_MODE=1 and TARGET_PACKAGES are set as environment variables. It downloads the original wheel for each target package, injects the .pth payload (which runs on Python interpreter startup), and publishes under a typosquatted name.
The typo generation is worth a look:
const MCP_SUFFIXES = ['-mcp', '-mpc'];export function generateTypos(name: string, limit = 5): string[] { return MCP_SUFFIXES.map((s) => `${name}${s}`).slice(0, limit);}
The comment in the source file says “keyboard proximity” but the code only appends -mcp and -mpc. This isn’t really typosquatting in the traditional sense – it’s exploiting the fact that PyPI blocks obvious misspellings of popular packages but allows suffix variants. And -mcp is topical. The MCP (Model Context Protocol) ecosystem has exploded over the last year, and developers are actively searching for packagename-mcp. Publishing a trojanized requests-mcp or boto3-mcp is plausible enough that people will install it without much scrutiny.
Lateral Movement
The worm supports several lateral movement vectors:
AWS SSM: if AWS credentials are found with Systems Manager access, SsmMutator enumerates managed EC2 instances across 17 AWS regions in parallel. The self-extracting payload gets base64-encoded, chunked into 24KB pieces (the SSM command size limit), and delivered via ssm:SendCommand using AWS-RunShellScript. The target reassembles via printf appends, decodes, and executes with node. Five instances are targeted concurrently with a 120-second timeout per command.
SSH: SshMutator handles lateral movement using SSH credentials, scp, and remote ssh exec. The architecture docs note this is currently untested.
GitHub Actions Runners: a GitHubActionsService provider dumps runner memory via /proc to extract secrets that aren’t exposed as environment variables. This is targeting the gap between what GitHub Actions secrets expose and what might be in the runner’s memory space from previous steps.
What the Architecture Notes Say About Gaps
The ARCHITECTURE.md is candid about what doesn’t work yet:
- PyPI trusted publishing-based spreading is untested
- SSH propagation via
scpandssh execis untested - JFrog Artifactory npm package infection is untested
- RubyGems trusted publishing is untested
- GCP and Azure providers for secret exfiltration don’t work
The DomainSenderFactory exists in the code. It would allow exfiltration to attacker-controlled C2 infrastructure but it’s not wired into the current sender chain. The worm explicitly avoids custom domains to dodge network monitoring.
The presence of INTEGRATION_TESTING.md suggests the gaps aren’t permanent. This is a codebase under active development.
What Defenders Should Do
I’m going to resist the urge to make this a generic “defense in depth” recommendation, because those are usually not useful. Here are the specific things that matter for this specific threat.
Monitor GitHub Actions for environment protection bypasses. The mutator clears required reviewer lists without restoring them. If you have GitHub org-level alerting on environment protection changes, that’s a detection point.
Treat AI tool config files as executable code. .claude/settings.json, .cursor/rules/*.mdc, .vscode/tasks.json, .gemini/settings.json – these files execute shell commands. They should be in your code review checklist for incoming pull requests, especially feature branches from forks.
Scrutinize binding.gyp files in npm packages. Legitimate packages use binding.gyp for native addons. It should compile C/C++ code. If you see binding.gyp executing node index.js as a build step with no actual native code, something is wrong.
The dead-man switch changes incident response procedures. If you find a DontRevokeOrItGoesBoom commit in your environment, do not immediately revoke the PAT. Isolate the machine first. Revoking the token while the dead-man switch is running on the victim machine triggers a destructive wipe.
Network monitoring looking for GitHub API calls is not going to catch this. The C2 traffic is indistinguishable from a developer using gh on the command line. Behavioral detection – watching what processes the GitHub API calls spawn is more useful than network-layer detection.
Provenance bundles are not enough. If the worm is running in a GitHub Actions context with OIDC access, it can attach a valid Sigstore provenance bundle to a trojanized package. The provenance is legitimate from an infrastructure standpoint; it just describes an infection event. You need to verify that the workflow that generated the provenance is the one you intended.
The Broader Pattern
I’ve been making the case for a while that the security industry’s mental model for supply chain attacks is stuck in 2020. The model was: attacker compromises a package, malicious code gets published, package manager installs it, victim runs it. The defenses built around that model – scanning package contents, checking hashes, watching for suspicious new versions are necessary but not sufficient.
Miasma is playing a different game. It propagates through:
- Pull request branches (code you haven’t merged yet)
- AI tool configurations (code your tools run, not code you run)
- GitHub Actions semver tags (code your CI pipeline trusts by reference)
- Legitimate OIDC-signed npm packages (code that passes provenance checks)
- Developer machine persistence (code that runs before you open your IDE)
The common thread is trust. The worm exploits trust relationships between developers and their tools, between CI pipelines and actions they reference, between the npm registry and its publish workflow. Each propagation vector is leveraging a trust signal that was designed for legitimate use.
The open-sourcing of this toolkit is going to produce forks, variants, and derivatives. Some of them will close the gaps that the ARCHITECTURE.md notes are untested. The PyPI trusted publishing path will get tested. The RubyGems path will get tested. The attackers who fork this don’t have to do the hard architectural work – they just have to fill in the untested modules.
The worm’s own ARCHITECTURE.md says it best: no dealing with takedowns or maintaining infrastructure. The GitHub commit search API is free, it’s public, it’s rate-limited but not meaningfully restricted, and it will almost certainly never be taken down. This is not an edge case exploitation – it’s using GitHub as designed, for purposes GitHub never intended.
That’s the problem. And it’s not going away.
The SafeDep team’s PMG project monitors package manager installs for malicious behavior at install time. The worm behaviors described in this post including binding.gyp execution triggers and OIDC-signed infected packages are among the patterns PMG’s sandbox detection is designed to catch.
This post first appeared at - The CyberSec Guru