
GlassWorm started as a VS Code supply chain attack. Now it's targeting MCP packages directly: the tools your AI agents trust and execute. Here's exactly what it does, and why runtime enforcement is the layer that still works after a malicious package is already installed and running.
In October 2025, Koi Security discovered a self-propagating worm spreading through the OpenVSX marketplace (the extension registry powering VS Code, Cursor, and Windsurf). They named it GlassWorm. By the time it was contained, it had reportedly compromised 35,800 developer machines. It came back in December with 24 fresh extensions.
Now it's back again, and it's aiming higher.
Koi Security recently disclosed three new malicious packages from the same campaign:
iflow-mcp/watercrawl-watercrawl-mcp(npm): brand new package, all five versions maliciousaifabrix/miso-client(npm): compromised in v4.7.2quartz-markdown-editor(VSCode): compromised in v0.3.0
Check your versions if you're using any of these. The target shifted from IDE extensions to MCP packages. That's not a random pivot. As Koi put it: "developers are installing MCP servers at scale and without oversight, making them perfect supply chain targets."
They're right. A compromised IDE extension runs alongside your agent. A compromised MCP server runs as your agent's tool, inside the execution context with whatever filesystem and network access you granted it at setup. The blast radius is larger.
What made GlassWorm remarkable from the start wasn't the payload. It was the evasion technique. GlassWorm encodes its malicious JavaScript using Unicode variation selectors: printable characters that produce no visual output in any code editor. To a human reviewer, the package looks like it has blank lines between legitimate code. The malicious instructions are invisible. Not obfuscated. Not encoded behind a layer of base64. Invisible.
Koi Security's CTO called it "the most sophisticated attack we've yet to investigate." The reason: the entire history of software supply chain security is built on the assumption that humans can read code to verify it. GlassWorm proved that assumption wrong.
The Kill Chain
GlassWorm operates in stages. Each stage is designed to survive partial detection.
Stage 0: Delivery
The worm enters through a legitimate-looking package on npm or a VS Code extension on OpenVSX. The latest wave targets MCP packages specifically: the npm packages your agent runtime installs to load tools. Attackers register new packages that impersonate popular MCP servers, or compromise existing packages at a specific version. They inflate download counts to push packages up in search results.
For VS Code extensions, auto-update delivers the malicious version without user interaction once the initial install happens. For npm MCP packages, any npm install or dependency update that pulls the compromised version is sufficient. There's no second prompt. The malicious version is just... there.
Stage 1: Credential Harvest
The first thing GlassWorm does is sweep the filesystem for credentials. It knows exactly where to look:
~/.git-credentials~/.gitconfig~/.config/git/credentials~/.npmrc(contains npm auth tokens)~/.aws/credentials~/.ssh/id_*- Local
.envfiles andsecrets/directories - Crypto wallet files across 49 recognized formats
These are standard open() and read() syscalls. Nothing exotic. The same filesystem primitives any legitimate extension uses, just aimed at credential-rich paths.
Stage 2: C2 Beacon
Once credentials are staged, GlassWorm phones home. The C2 infrastructure is deliberately resilient:
- Primary: Solana blockchain RPC endpoint. The actual C2 server address is encoded as data in a public wallet. No domain to take down, no IP to block.
- Fallback: A Google Calendar event, parsed for the encoded C2 address.
Both channels use HTTPS on port 443. Both look like normal outbound web traffic. The malware issues connect() syscalls to these endpoints and exfiltrates the harvested credentials.
Stage 3: ZOMBI Module
The final payload, ZOMBI, turns the infected machine into a node in a criminal infrastructure network. It drops:
- SOCKS proxy: listening on a local port, providing the attacker's network a bounce point
- HVNC server: hidden VNC for remote desktop access, established through an outbound tunnel
- WebRTC module: peer-to-peer C2 channel that bypasses traditional firewall inspection
- BitTorrent DHT: decentralized command distribution, no single chokepoint
All of this requires binding local ports, spawning child processes, and establishing outbound connections to attacker-controlled endpoints.
Stage 4: Propagation
With stolen npm tokens and GitHub credentials, GlassWorm authenticates to those registries and pushes infected versions of packages the victim has publish rights to. Each new victim is an infection vector. The worm scales.
Why Pre-Execution Controls Don't Work Here
The standard playbook for supply chain security is:
- Audit extensions and packages before install
- Pin to known-good versions
- Review code changes before approving updates
- Use a curated internal registry
GlassWorm puts pressure on all four. The invisible Unicode trick undermines (1) and (3) by making malicious code invisible to normal review workflows: you're reviewing code you literally cannot see. Auto-updates erode (2) unless you lock versions and manually approve every update. Curated internal registries help, but the initial package approval still requires human review of code that is, by design, unreadable.
The deeper problem: all pre-execution controls operate on the representation of code: what it looks like in an editor, what a static analyzer can parse, what a human can read.
Runtime enforcement operates on what the code actually does: the kernel operations it issues once it is already running. It doesn't replace dependency auditing or registry controls. It's the layer that still works after those controls have been bypassed.
Once the package is running, the question is no longer whether the source looked legitimate. The question is which kernel operations it is allowed to perform.
How AgentSH Stops It
No tool is uniquely magical here. Once a malicious package is installed and running, the decisive control point is the layer where it touches the kernel. That's what syscall-level enforcement gives you.
AgentSH sits under the agent runtime at the syscall level, using a combination of seccomp, Landlock, and kernel interception on Linux, enforcing policy on every file operation, network connection, and process spawn. It doesn't parse source code. It intercepts kernel calls. Windows WSL2 is fully supported; native macOS and Windows support are coming soon.
Here's what happens when GlassWorm runs inside an AgentSH-protected environment.
Stage 1: Credential Harvest → Denied at open()
The malware calls open("/home/user/.git-credentials", O_RDONLY). AgentSH intercepts the syscall before it completes. The path matches the deny-git-credentials rule in the default policy:
- name: deny-git-credentials
paths:
- "/home/**/.git-credentials"
- "/home/**/.gitconfig"
- "**/.git/config"
operations: [read, write]
decision: deny
The call returns EPERM. The malicious code never gets the token. Same result for ~/.aws/credentials, ~/.ssh/id_*, .env files, all covered by explicit deny rules in the default policy (with exact path coverage depending on your environment). The credential sweep fails completely.
The audit log captures every attempt: path, process, PID, timestamp, matched rule. You know exactly what the malware tried to touch.
Stage 2: C2 Beacon → Blocked at connect()
GlassWorm tries connect() to the Solana RPC endpoint. That domain isn't in AgentSH's network allowlist. Unknown destinations on port 443 are sent to an interactive human approval gate: the connection is held until someone approves it, and denied if the timeout expires:
- name: approve-unknown-https
ports: [443]
decision: approve # connection is blocked pending explicit human approval
message: "Agent wants to connect to: :"
timeout: 2m
- name: default-deny-network
domains: ["*"]
decision: deny
The beacon never goes out. You can also add a targeted block to be explicit:
- name: deny-glassworm-c2
domains:
- "*.solana.com"
- "api.mainnet-beta.solana.com"
- "calendar.google.com" # GlassWorm's fallback C2
decision: deny
priority: 100
The Google Calendar fallback is worth calling out: it's a deliberate attempt to hide C2 traffic inside a domain most organizations trust. An explicit deny on the fallback endpoint closes that channel without forcing you to block all Google traffic.
Stage 3: ZOMBI Module → Blocked at Process and Network
ZOMBI's components each require syscalls that AgentSH controls directly:
The SOCKS proxy needs to bind() and listen() on a local port, then relay traffic outbound. AgentSH's command rules restrict which processes can bind ports, and the outbound relay connections to attacker infrastructure fail the same network policy that blocked Stage 2.
The HVNC tunnel requires an outbound connect() to an attacker-controlled host; it is not in the allowlist. Same outcome.
The WebRTC and DHT modules require spawning child processes. Command rules restrict process execution to an explicit allowlist; unknown binaries are denied or require approval before running.
Process limits (pids_max: 100 via cgroups) constrain the subprocess tree as a secondary backstop, but the primary controls are the targeted syscall denies above.
Stage 4: Propagation → No Credentials to Use
The self-propagation depends entirely on the credentials harvested in Stage 1. Those were never read. There's nothing to authenticate with.
Even if credentials somehow existed in memory, publishing to npm requires outbound access to registry.npmjs.org over an authenticated HTTPS session. That connection requires an explicit allowlist entry or human approval. The same network policy that stopped the C2 beacon applies equally to the registry push.
The worm has no vectors left.
The Audit Trail
Every blocked operation generates a structured audit event. After a GlassWorm infection attempt in a protected environment, the log looks like:
{"ts":"2025-10-18T14:23:11Z","event":"file_denied","path":"/home/dev/.git-credentials","op":"read","pid":9821,"comm":"node","rule":"deny-git-credentials"}
{"ts":"2025-10-18T14:23:11Z","event":"file_denied","path":"/home/dev/.npmrc","op":"read","pid":9821,"comm":"node","rule":"deny-env-files"}
{"ts":"2025-10-18T14:23:12Z","event":"net_denied","remote":"api.mainnet-beta.solana.com:443","pid":9821,"comm":"node","rule":"default-deny-network"}
{"ts":"2025-10-18T14:23:12Z","event":"net_denied","remote":"calendar.google.com:443","pid":9821,"comm":"node","rule":"default-deny-network"}
You don't just block the attack. You document every step of it. That's the forensic record you need to understand what was targeted, what exfiltration was attempted, and whether similar activity is happening elsewhere in the fleet.
The Principle
GlassWorm is not a novel attack. The Unicode trick is clever, but what it exploits is fundamental: every security control that operates before execution is blind to what code actually does.
Pre-execution controls (static analysis, human review, code signing, curated registries) defend against the representation of intent. They're necessary. They're not sufficient. GlassWorm is the proof of concept. The real attacks will be harder to name.
Try AgentSH
AgentSH is open source. The default policy blocks the key behaviors GlassWorm relies on: credential reads, unknown outbound connections, and unauthorized process and port activity.
GitHub: github.com/canyonroad/agentsh
The policy is a single YAML file. Start with the default, tighten it for your environment.
If this was useful, a star on the repo goes a long way.
← All postsBuilt by Canyon Road
We build Beacon and AgentSH to give security teams runtime control over AI tools and agents, whether supervised on endpoints or running unsupervised at scale. Policy enforced at the point of execution, not the prompt.
Contact Us →