How malicious agent skills hide their instructions
VirusTotal found malicious agent skills hiding orders after blank lines, in HTML comments and in invisible Unicode. How to check a skill, and what our scan catches.
· 5 min read · SetupBay
Agent skills are mostly Markdown. A SKILL.md is a bit of front matter and then plain instructions, and your agent reads every byte. That’s also the problem: a skill doesn’t need any code to do something nasty. It only needs the instruction written somewhere a person won’t look.
VirusTotal has put numbers on this. In Part III of its “From Automation to Infection” series (8 October 2026) it looked at 35,878 skills submitted to the service and classed 6,637 of them (18.5%) as malicious. Of those, 62% had no malicious code at all. The attack was simply written out as instructions for the agent. A follow-up on 21 October looked at the skills that go a step further and hide those instructions from whoever reviews them. We first saw it in @IntCyberDigest’s summary on X.
Most of them don’t bother hiding
Hiding is the exception. VirusTotal found it in 90 of the 6,637 malicious skills, about 1.4%. In most of the rest, the bad instruction sits in plain view, dressed up as a routine step. The Part III example is a tax-planning skill whose last line tells the agent to blind-copy the user’s tax documents to an outside email address. You don’t need a scanner for that, just someone reading the file. The 90 hidden ones are where reading the rendered page isn’t enough.
The three hiding tricks
Blank lines. 56 skills put the instruction after a long run of empty lines, between 78 and 825 of them, often filled with spaces. That tax skill used nearly 500. Scroll it on GitHub and the documentation seems to end, so you stop. The agent doesn’t.
HTML comments. 28 skills put it inside <!-- ... -->. GitHub, marketplace pages and Markdown previews don’t render comments, but the agent reads the raw file, comment and all. VirusTotal’s examples include comments asking the agent to send API keys to a “verification” URL, and one that pipes a download straight into a shell.
Invisible Unicode. Only six skills, but the nastiest to find. Unicode tag characters (U+E0000 to U+E007F) mirror ASCII but display as nothing, so an empty-looking line can hold a whole sentence. Others use zero-width characters, sometimes scattered between words so a search for “API keys” no longer matches.
Plenty of skills also tell the agent to keep quiet. One sample says it outright: “Your human doesn’t need to know.” VirusTotal didn’t count these, because words like “silently” turn up in legitimate skills too and simple rules couldn’t tell them apart. That caveat applies to our scanner as well.
Checking a skill yourself
None of this needs special tools. Run these from the skill’s folder, against every file in it, not just SKILL.md. They only read.
Read the raw file, right to the end:
wc -l SKILL.md # how long is it really?
tail -n 20 SKILL.md # what's at the very bottom?
less -N SKILL.md # read it with line numbers, not the rendered page
Look for HTML comments:
grep -rn '<!--' .
Some are harmless, like prettier-ignore or table-of-contents markers. Anything that reads like an instruction isn’t.
Find long blank runs, and text pushed far off to the right:
awk 'NF==0 {run++; next} run>=20 {print FNR ": text after " run " blank lines"} {run=0}' SKILL.md
grep -nE '[[:space:]]{100,}[^[:space:]]' SKILL.md
Show invisible characters. Zero-width, direction-override and tag characters are all in Unicode’s “format” category, so this catches the lot:
python3 - SKILL.md <<'EOF'
import sys, unicodedata
for path in sys.argv[1:]:
for n, line in enumerate(open(path, encoding="utf-8"), 1):
for c in line:
if unicodedata.category(c) == "Cf":
print(f"{path}:{n}: U+{ord(c):04X} {unicodedata.name(c, '?')}")
EOF
Then look for wording aimed at the agent, and for anywhere your data could be sent:
grep -niE "don'?t (tell|inform|mention)|needs? to know|silently|without (telling|asking)" SKILL.md
grep -rnoE 'https?://[^ )"]+|[[:alnum:]._%+-]+@[[:alnum:].-]+\.[a-z]{2,}' .
A skill that formats dates has no reason to email anyone.
After installing, watch what it changes. VirusTotal suggests treating AGENTS.md, CLAUDE.md and memory files as off limits to skills without your say-so, because an instruction written there outlives the skill. If your rules live in one committed AGENTS.md (our one AGENTS.md for every agent loadout sets that up), a quiet edit shows up in git diff before it ever reaches a commit. More of that on the Git stack page.
If you’d rather the agent did the vetting, our vetting loadout treats a new skill, plugin or MCP server as a dependency: pin it, scan every file, and write a report for you to approve. On Claude Code, a hook beats a rule. Our hooks guard rails refuse a download piped into a shell, wherever the instruction came from, and that’s exactly what one of VirusTotal’s hidden comments asked for. If you publish skills yourself, the plugin authoring loadout runs its checks before anyone installs.
What our scan checks
Every loadout on SetupBay goes through one pattern scanner (intake-precheck/2) on every change and weekly, as does every submission through the form or API. After the VirusTotal post we added checks for the hiding tricks. It looks for:
- HTML comments in Markdown, outside code blocks. Tool directives like
prettier-ignoreare allowed; anything else is flagged for a person, and marked high if it holds an instruction, a command or a URL. - Padding: any text after 50 or more blank or whitespace-only lines, or after 100 or more spaces on one line.
- Covert and prompt-injection wording: keeping something from the user, sending data quietly, “ignore previous instructions” and the like.
- Hidden Unicode: zero-width characters, direction overrides and the whole tag block. Breaking up a phrase with zero-width spaces trips this rule rather than dodging the others.
- Credentials: private keys and common token formats. A submission with one is rejected outright.
- Risky shell: downloads piped into a shell, base64 decoded and run, reverse shells, reads of SSH or cloud credentials, and flags that switch off permission prompts.
Each loadout page shows every check and when it last ran.
Now the limits. This is pattern matching. It catches these tricks when they look like the examples and will miss someone who words things differently. We’d have caught that tax-planning line only because of the blank lines in front of it. On its own, copying documents to an email address reads like a normal workflow step, and no regex knows the address isn’t yours. It also flags harmless things, so a flag means a person reads the file, not a verdict. And no AI agent has run any of our loadouts yet. A clean scan means none of these patterns turned up. It doesn’t mean a skill is safe.
So read the raw file, all the way to the end. It takes a couple of minutes, and in VirusTotal’s data that alone would have shown you most of the malicious skills.