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:
| Source | Sample | Finding |
|---|---|---|
| Academic analysis | 42,447 skills | 26.1% had ≥1 security vulnerability |
| Snyk audit, Feb 2026 | 3,984 ClawHub skills | 36.8% flawed · 13.4% critical · 76 confirmed malicious |
| ClawHavoc campaign, Jan–Feb 2026 | OpenClaw marketplace | 1,200+ malicious skills published |
| Unit 42, Feb–May 2026 | ClawHub | 5 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+E0000–U+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
Comments