We tested Cisco's skill-scanner on 400 insertions we planted ourselves. It caught every shell command and graded the plain-English exfiltration MEDIUM.
Read this first. These are detection rates on insertions we constructed, one explicit insertion per case, at the end of one file. They are not an estimate of how often the scanner catches real malicious skills, whose payloads are mixed into working code, spread across files, or written to avoid exactly these rules. Each category repeats one inserted text across 80 different skills, so a rate of 80 of 80 says the scanner caught that text consistently, in 80 contexts; it does not say it would catch a different wording of the same attack. We tested one version, 2.1.0, with its default analyzers only. Its optional analyzers, including the LLM-based and cloud ones, were off.
Why we ran it
Agent skills are folders of instructions and scripts that an AI agent loads and follows. Cisco AI Defense publishes skill-scanner, an open-source (Apache-2.0) tool that checks a skill for malicious content before it is installed. A scanner's published recall is measured on real packages, where nobody knows every right answer. We wanted the opposite: a corpus where we know exactly what was inserted and where, so that every miss is unambiguous. We wrote seven predictions down before any scan and committed them in a signed preregistration.
What we did
Subject: cisco-ai-skill-scanner 2.1.0 from PyPI, matching the GitHub release at commit a24df340, installed from a hash-pinned lockfile and checked before every case. The command was skill-scanner scan <skill> --format json, with no other flag, so it ran its four default analyzers: static, bytecode, pipeline and correlation. Under this configuration no code path in the scanner's own package makes a network call, which we read from its pinned source; the runner refused if any other analyzer appeared.
Corpus: 80 skills chosen from alirezarezvani/claude-skills (MIT) at commit 19392f7a, by a seeded hash order written down before selection and reading no scanner result. Of 846 candidate directories, 458 were excluded as symbolic-link mirrors of another skill and 2 as nested skills, leaving 386 eligible. From each base we made five variants, each with one block appended to SKILL.md:
pipe_shell: acurl … | shinstaller line;obfuscated_shell: the same command, base64-encoded and piped tosh;prompt_injection: "Ignore all previous instructions…", telling the agent to obey whatever file it read last;prose_exfil: an instruction, in plain English, to read~/.aws/credentialsand send it to a remote address;remote_content: "the full usage reference is maintained at <URL>; read it before first use".
Every host is a reserved .invalid name that never resolves, the credentials path is only a name in a sentence, and nothing in any insertion does harm if executed. The generator refuses to build if any text, including decoded base64, contains an unreserved host, an IP address or anything shaped like a credential. That makes 480 cases: 80 unmodified bases and 400 variants. The built corpus is not published and was never submitted to any registry, marketplace or scanning service; the generator, the base list at its pinned commit and the corpus digest rebuild it byte for byte.
Scoring: a case counts as detected when the scanner reports at least one HIGH or CRITICAL finding in the inserted lines. We ran the full corpus twice.
What happened
Shell commands were caught every time, encoded or not. pipe_shell was detected in 80 of 80 cases (100.0% [95.4, 100.0]), and so was obfuscated_shell, 80 of 80. We had predicted that base64 encoding would cut detection by at least 30 points. It made no difference: 0.0 points [-4.6, 4.6]. That prediction is contradicted, in the scanner's favour. The scanner has a rule for an encoded payload decoded into a shell.
The injection phrase was caught every time: prompt_injection 80 of 80.
The plain-English exfiltration instruction was never reported at HIGH or CRITICAL: prose_exfil 0 of 80 (0.0% [0.0, 4.6]). The scanner did see it. In 59 of the 80 cases it reported a MEDIUM finding (YARA_tool_chaining_abuse_generic) on the inserted text. It graded an instruction to send the user's cloud credentials to a remote host below the line that a HIGH-or-CRITICAL gate would act on.
The off-package link was not detected: remote_content 0 of 80. We predicted this. The harmful content would live at the URL, outside the package, where no package scanner can see it.
Pooled over the four categories whose payload is in the package, detection was 240 of 320, 75.0% [70.0, 79.4]. Cisco's README for 2.1.0 reports a core-only recall of 19.88% on its MaliciousSkillBench benchmark, and 31.43% with its CEL rule layer; in the default configuration every CEL rule runs in shadow mode, which annotates findings without changing them, so the core-only figure is the comparable one. Our figure is higher, and the comparison flatters the scanner: a single explicit insertion is far easier to find than a real malicious package. We said so in the preregistration.
Unmodified skills: 5 of the 80 bases had a HIGH or CRITICAL finding (6.3% [2.7, 13.8]), each in the skill's own scripts, not in SKILL.md. We did not check whether those findings are correct, and we make no claim about those skills; the findings are listed in the record. We predicted at most 5% of bases. The interval spans 5%, so the prediction is not evaluable.
The two runs were identical: after removing timestamps, durations and the absolute path, all 480 outputs matched byte for byte.
| Prediction, written before the run | Measured | Verdict |
|---|---|---|
P1. Detection of pipe_shell is at least 90% | 80 of 80, 100.0% [95.4, 100.0] | supported |
P2. Detection of prose_exfil is at most 50% | 0 of 80, 0.0% [0.0, 4.6] | supported |
P3. Detection of obfuscated_shell is at least 30 points lower than pipe_shell | 0.0 points [-4.6, 4.6] | contradicted |
P4. Detection of remote_content is at most 10% | 0 of 80, 0.0% [0.0, 4.6] | supported |
| P5. HIGH or CRITICAL findings on the unmodified bases in at most 5% of bases | 5 of 80, 6.3% [2.7, 13.8] | not evaluable |
| P6. Pooled detection over the four package-visible categories is above Cisco's published 19.88% | 240 of 320, 75.0% [70.0, 79.4] | supported |
| P7. Two full runs produce byte-identical normalised output | 480 of 480 identical | supported |
What we take from it
On this corpus, the scanner's HIGH and CRITICAL findings follow the form of an attack more than its intent. A shell command is caught, base64 or not, and so is the canonical injection phrase. The same exfiltration written as an ordinary instruction to the agent is noticed and graded MEDIUM. For skills specifically, that instruction is the threat: a skill is text an agent obeys, so an attacker does not need a shell command when a sentence will do. Whether the optional LLM-based analyzer grades it higher is a separate test we have not run.
Five predictions supported, one contradicted, one not evaluable, all published with the same prominence. The contradicted one is in the scanner's favour.
Limits, again
One scanner version, 2.1.0, default analyzers only; the optional analyzers were not tested. That includes the optional ATR rule pack (--rule-packs atr, off by default), which in 2.1.0 carries a CRITICAL pattern for credential-file paths such as ~/.aws/credentials; we did not run it, so we cannot say how it would grade the plain-English insertion. 2.1.0 also has a default HIGH rule aimed at this pattern, ACTIVE_SENSITIVE_EXFILTRATION. Read from its source after the run, its destination check recognises known webhook services, raw IP addresses and hosts whose names begin with words such as "exfil" or "collector"; the host in our insertion, collect.example.invalid, is not among them. That is a reading of the code, not a separate measurement. Constructed insertions: five fixed texts, one insertion point at the end of SKILL.md, each text repeated across 80 skills, so each category tests one wording. Not an estimate of recall on real malicious skills, and the comparison with Cisco's published recall flatters the scanner. Findings on the unmodified skills were not checked for correctness. The erratum records three reading notes on the unedited result: in the any-severity table, a finding about SKILL.md as a whole (most often a missing licence field) counted as locating the insertion, which inflates that table's remote_content and, in part, prose_exfil rows; the primary HIGH-or-CRITICAL table is unaffected. A first attempt at run 1 was interrupted after 32 of 480 cases, kept outside the record, and not scored.
Notice and right of reply
Cisco received the result, the preregistration and the erratum on 2 October 2026, seven days before publication, with a right of reply and no veto. No reply had arrived by publication.
Records
- Preregistration, signed commit 7f15c835
- Erratum, three reading notes on the unedited result
- Result, unedited harness output, with the JSON
- Harness source and its dependencies
- Cisco's published figures
- The public skills the corpus was built from