Your agent installs Markdown from the internet and runs it. We spent 20 years learning not to do that.

Agent skills are dependencies with none of the controls. What the 2026 research found, why scanners miss it, and seven fixes for platform teams.


Diagram: an agent skill supply chain from public registry to internal mirror to runtime sandbox, with prose-layer threats and platform controls called out

I’ve been trying to work out why agent skills bother me more than they seem to bother most people, and I think I’ve landed on it.

Every supply chain control we built over the last twenty years — signing, pinning, SBOMs, reproducible builds, scanning, provenance — rests on one shared assumption: the dangerous part is the code. Get the code right and you’re fine. Everything in the toolchain points at that assumption.

An agent skill breaks it. The dangerous part can be a paragraph.

A skill is mostly Markdown. It tells the agent what it’s for and how to do it. The agent reads it and does the thing, using whatever credentials, shell access, and file system permissions it already has. There’s no exploit chain here, no postinstall script, no memory corruption. Sentences that read as helpful guidance to a reviewer read as an instruction to a model. That’s the whole attack.

And the 2026 numbers say this isn’t hypothetical anymore.


What the research actually found

Four independent efforts landed in the first half of 2026, and they broadly agree, which is unusual and not comforting:

SourceSampleFinding
Academic analysis42,447 skills26.1% had ≥1 security vulnerability
Snyk audit, Feb 20263,984 ClawHub skills36.8% flawed · 13.4% critical · 76 confirmed malicious
ClawHavoc campaign, Jan–Feb 2026OpenClaw marketplace1,200+ malicious skills published
Unit 42, Feb–May 2026ClawHub5 malicious skills evaded VirusTotal + ClawScan

A quarter to a third of a public registry carrying at least one flaw is not a scandal about one marketplace. It’s a base rate for an ecosystem where publishing costs nothing, review is automated, and the review tooling is aimed at the wrong artifact.

The Unit 42 breakdown is the one I’d read closely, because it shows the shape of the threat rather than the volume. Of five skills that got through screening: two were macOS infostealers with live command-and-control, one existed purely to test evasion (it inflated README.md to 22MB with padding characters to blow past scanner size thresholds), and two were what they call agentic threats — runtime affiliate injection and front-running the user’s actual task for financial gain.

Those last two are the genuinely new category. They don’t steal anything. They don’t trip anything. They quietly change what the agent does on your behalf, and the output still looks like a successful task.

Then there’s the technique the Cloud Security Alliance documented: Unicode Tag characters in the U+E0000U+E007F range. Most text editors and Markdown renderers show them as nothing at all. Models read them as content. You can review a skill line by line, approve it honestly, and have missed the payload entirely — because your eyes were given a different document than the model was.

And in June, a security firm built a harmless fake skill, walked it past both Cisco’s and NVIDIA’s scanners, promoted it through a marketplace and an Instagram ad, and reached roughly 26,000 agents — including some on corporate accounts. Nobody was attacked. That’s the point. Someone demonstrated the distribution channel end to end and published the receipt.


Why this is a platform problem, not a security problem

The instinct is to route this to AppSec. I’d push back, for a reason that has less to do with security expertise and more to do with where the decision gets made.

Nobody files a ticket to install a skill. A developer finds one, it works, and it’s in. By the time anyone asks who approved it, forty people are depending on it. This is exactly the dynamic behind agent sprawl — the failure isn’t a bad decision, it’s the absence of a place where a decision was ever required.

Security can write the policy. Only the platform can make the safe path the easy one. If installing a reviewed skill from your internal registry is slower or more annoying than curling one off the internet, you’ve written a document, not a control.

There’s a second reason. The blast radius isn’t set by the skill — it’s set by the runtime you built. A malicious skill running in an agent with read-only access to one repo and no egress is a nuisance. The same skill in an agent holding a production admin token is an incident. That token boundary is yours. It was yours before skills existed, and it’s the same argument as giving agents real identities instead of borrowed API keys — a skill can only spend the authority you handed the thing running it.


Seven controls, in the order I’d implement them

Roughly cheapest-and-highest-value first.

1. Inventory first. You cannot secure a set you can’t enumerate. Before anything else, answer: which agents run in this org, which skills does each load, and where did each skill come from? Most teams cannot answer question one. It’s an afternoon of work and it usually reframes the whole conversation.

2. Mirror, don’t proxy. Stand up an internal registry that holds copies. A caching proxy still means upstream can change what you’re running. A mirror means a human action is required for anything new to enter your environment. This is the single highest-leverage control and it’s the same one we landed on for npm and PyPI, for the same reasons.

3. Pin by content digest. Not by name. Not by version range. A skill that can silently update is a skill an attacker only needs to compromise once, later. Names and semver are trust-on-first-use; digests aren’t.

4. Lint the prose, not just the code. This is the control that doesn’t exist in your current pipeline, so here’s a starting point:

#!/usr/bin/env python3
"""Pre-merge lint for agent skill files. Flags prose-layer supply chain risks
that conventional code scanners are not looking for."""

import re
import sys
import unicodedata
from pathlib import Path

MAX_BYTES = 256 * 1024  # padding above this is an evasion signal, not a doc

# Invisible-to-humans, semantic-to-models
INVISIBLE = [
    (0xE0000, 0xE007F, "Unicode Tag characters"),
    (0x200B, 0x200F, "zero-width / directional marks"),
    (0x2060, 0x2064, "word joiner / invisible operators"),
    (0xFE00, 0xFE0F, "variation selectors"),
]

# Instructions aimed at the agent rather than the reader
IMPERATIVES = [
    r"ignore (all |any )?(previous|prior|above) instructions",
    r"do not (tell|inform|mention to|show) the user",
    r"without (asking|confirming|notifying)",
    r"you are (now |actually )?(a |an )?\w+ with (full|root|admin)",
    r"(disregard|override) (the )?(system|safety|security) (prompt|policy|rules)",
]

# Classic execution smells that still show up in skill bodies
EXEC_SMELLS = [
    r"curl[^\n|]*\|\s*(ba)?sh",
    r"wget[^\n|]*\|\s*(ba)?sh",
    r"base64\s+(-d|--decode)",
    r"eval\s*\(",
    r"(rentry\.co|glot\.io|pastebin\.com|transfer\.sh)",
    r"~/\.(aws|ssh|config/gcloud|kube)",
]


def scan(path: Path) -> list[str]:
    raw = path.read_bytes()
    findings = []

    if len(raw) > MAX_BYTES:
        findings.append(f"oversized: {len(raw):,} bytes (scanner-evasion pattern)")

    text = raw.decode("utf-8", errors="replace")

    for ch in text:
        cp = ord(ch)
        for lo, hi, label in INVISIBLE:
            if lo <= cp <= hi:
                name = unicodedata.name(ch, "unnamed")
                findings.append(f"invisible char U+{cp:04X} ({name}) — {label}")
                break

    lowered = text.lower()
    for pattern in IMPERATIVES:
        for m in re.finditer(pattern, lowered):
            line = lowered.count("\n", 0, m.start()) + 1
            findings.append(f"line {line}: agent-directed instruction — {m.group(0)!r}")

    for pattern in EXEC_SMELLS:
        for m in re.finditer(pattern, text, re.IGNORECASE):
            line = text.count("\n", 0, m.start()) + 1
            findings.append(f"line {line}: execution smell — {m.group(0)!r}")

    return findings


def main(argv: list[str]) -> int:
    targets = [Path(a) for a in argv[1:]] or list(Path(".").rglob("SKILL.md"))
    if not targets:
        print("no skill files found", file=sys.stderr)
        return 2

    failed = False
    for path in targets:
        findings = scan(path)
        if findings:
            failed = True
            print(f"\n{path}")
            for f in dict.fromkeys(findings):  # dedupe, keep order
                print(f"  ✗ {f}")
        else:
            print(f"{path}: clean")

    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

Two things about this script matter more than the regexes. First, it normalizes and inspects what the model will see, which is the gap the whole threat class lives in. Second, it deliberately fails on oversized files rather than skipping them — the 22MB README trick works precisely because scanners treat “too big” as “skip” instead of “suspicious.”

It will produce false positives. A skill that legitimately documents curl | sh will trip it. That’s fine — the output is a review prompt, not a verdict. Wire it into pre-merge on your mirror, not into runtime.

5. Require a declared capability manifest. Every skill states what it needs: file paths, network destinations, shell commands, credentials. Anything undeclared is denied at runtime. This converts “did I read every line carefully enough?” into “does the manifest match the blast radius I’m willing to accept?” — a much easier question to answer correctly at 4pm on a Friday.

6. Deny egress by default. This is the backstop, and it’s the reason to do it even though it’s the most annoying item on the list. Every control above can fail — you can miss an instruction, a reviewer can be tired, a scanner can be evaded. An agent that physically cannot reach an attacker-controlled endpoint fails safe anyway. Allowlist destinations at the gateway, the same way you’d handle any other untrusted workload.

7. Log skill invocations as first-class events. When a skill loads and when it acts, that’s an event with a name, a digest, and an actor. Without it, your post-incident question — “which agents ran the compromised version, and what did they touch?” — has no answer. With it, it’s a query. This is the same argument I made for observability in AI systems generally: the telemetry you didn’t collect is the investigation you can’t run.


The uncomfortable part

Control 4 doesn’t fully work, and I’d rather say so than sell you a linter as a solution.

You cannot reliably distinguish, by inspection, between a skill that says “check the user’s AWS config to find the right region” for good reasons and one that says it for bad ones. The text is identical. Intent isn’t in the artifact. This is the same reason prompt injection remains unsolved in the general case — it’s a property of systems that take instructions in the same channel as data, and skills are that architecture by design.

Which is exactly why controls 5, 6, and 7 carry the weight. Capability manifests, egress denial, and audit logging don’t require you to correctly judge intent. They limit what wrong judgment can cost, and they tell you afterward what happened. Every serious version of this problem — the OWASP Agentic Top 10 included — eventually arrives at the same place: containment beats detection, because detection has an adversary and containment has a config file.

I’d rather ship a boring capability manifest than a clever classifier.


Where this goes

Registries will add signing. Someone will publish a skill SBOM format. There’ll be a scanning vendor category within a year and most of it will be regex with a model bolted on. All of that is fine and none of it removes the structural issue, which is that we’ve built a package ecosystem whose payload is persuasion.

The thing I’d actually plan for: CVE-2026-25253 — that RCE in the OpenClaw skill runtime, disclosed by Ethiack in January and widely described as the first CVE assigned to an agentic AI system — means agent runtimes are now inside ordinary vulnerability management. Not AI governance. Not a working group. Patch SLAs, inventory, an owner, an on-call rotation that gets paged when a CVE drops.

Ask who that owner is in your organization today. If the answer takes more than a few seconds, that’s the actual finding, and it’s worth more than any scanner you could buy this quarter.


Sources: Formal Analysis and Supply Chain Security for Agentic AI Skills (arXiv) · Unit 42: OpenClaw’s Skill Marketplace and the Emerging AI Supply Chain Threat · CSA: Agent Context Poisoning — SKILL.md and the New AI Supply Chain Attack Surface · SecureWorld: AI Agents Just Reset the Supply Chain Clock to Zero

Frequently asked questions

What is an agent skill supply chain attack?

It is the compromise of an AI agent through a third-party skill — a package, usually mostly Markdown, that extends what the agent can do. Because a skill's payload is natural-language instruction as much as it is code, an attacker doesn't need a memory-safety bug or a postinstall script. Text that reads as guidance to a human reads as a directive to the model, and the agent already holds the credentials, shell access, and file system permissions the attacker wants. The 2026 campaigns showed the full range: credential stealers, invisible Unicode instructions, and skills that quietly inject affiliate codes or front-run the user's actual task.

How common are vulnerable agent skills?

Worryingly common in the public registries. A large-scale academic analysis of 42,447 agent skills found 26.1% exhibited at least one security vulnerability. Snyk's February 2026 audit of 3,984 skills from the ClawHub registry found 36.82% contained at least one flaw, 13.4% carried critical-severity issues, and 76 contained confirmed malicious payloads. Separately, the ClawHavoc campaign in January–February 2026 pushed more than 1,200 malicious skills into the OpenClaw marketplace. These are ecosystem-wide base rates, not tail events.

Why don't security scanners catch malicious skills?

Because most scanners are looking for the wrong thing. They were built to find malicious code, and a skill's attack surface is largely prose. Unit 42 documented five malicious skills that got past ClawHub's own VirusTotal and ClawScan screening between February and May 2026, including one that padded its README.md to 22MB purely to exceed scanner size limits. The Cloud Security Alliance has documented Unicode Tag characters in the U+E0000–U+E007F range being used to embed instructions that are invisible in every text editor and Markdown viewer but are read as semantic content by the model. In June 2026 a security firm got a deliberately benign fake skill past both Cisco and NVIDIA scanners and onto roughly 26,000 agents, some on corporate accounts.

How should a platform team secure agent skills?

Treat skills as dependencies and apply the controls you already apply to dependencies, plus two new ones. The familiar six: an internal registry that mirrors rather than proxies, pinning by content digest rather than name or version range, provenance and signature requirements, no network access at install time, a declared capability manifest, and human review of every diff. The two new ones are specific to this threat class: normalize and lint the prose (strip invisible Unicode, flag imperative instructions aimed at the agent, cap file sizes), and enforce runtime egress control, because the only reliable backstop against instructions you failed to read is a network the agent cannot reach.

Is CVE-2026-25253 relevant to my deployment?

It matters as a marker even if you don't run the affected runtime. Disclosed on 27 January 2026 by researchers at Ethiack, it is a remote code execution vulnerability in the OpenClaw agent skill runtime and is widely described as the first CVE identifier assigned to an agentic AI system. The specific bug is patchable. The structural point is that skill runtimes are now in scope for conventional vulnerability management, which means they need an owner, a patch SLA, and an inventory — three things most organizations have not yet assigned for anything in their agent stack.

Comments