先说赛道:2026 年 3 月起,skill 成了新的 npm

工坊写过 agent-skills、agentic-awesome-skills、superpowers——那时大家关心的是「去哪找 skill」。2026 年上半年冒出来的新问题是「装的这个 skill 有没有毒」。SkillSpector 引用的那篇研究给了个足够吓人的数字:42,447 个 skill 的实证研究里,26.1% 至少含一个漏洞,5.2% 表现出明显恶意意图,而带可执行脚本的 skill 中招概率是纯文档 skill 的 2.12 倍。

这个恐惧催生了一条清清楚楚的赛道,而且是大厂集体下场:

项目Star出身 / 建仓路线
NVIDIA/SkillSpector17,069NVIDIA,2026-03LangGraph 27 节点扇出 + YARA/AST/污点 + LLM 精修,接 NVIDIA 签名与 skills catalog
Tencent/AI-Infra-Guard6,255腾讯,2024-12全栈 AI 红队平台:Agent Scan + Skills Scan + MCP Scan
snyk/agent-scan3,034Snyk,2025-04「发现并扫描你机器上所有 agent 组件」,uvx 一把梭,README 自己标 CLI 输出 experimental
cisco-ai-defense/skill-scanner2,518Cisco,2026-01YAML + YARA-X 模式、AST/数据流、可选 LLM 裁判,外加一层 cel-go 有界决策层;SARIF 进 GitHub Code Scanning、有 pre-commit hook

四家的技术路线其实高度重合(模式匹配 + AST/污点 + LLM 语义 + SARIF),差别在工程取向:Snyk 主打「扫全机器」,腾讯主打「一个平台全包」,Cisco 主打「进 GitHub Code Scanning 的 CI 集成」,NVIDIA 主打「扫完给签名、进官方目录」——它不只是个扫描器,是 NVIDIA 那个 skill 应用商店的质量闸门,这也是它 Star 最高的原因之一。

它是怎么实现的:27 个分析器扇出,一台 LangGraph 流水线

装完之后我直接扒了包内的源码(uv tool install 装到 ~/.local/share/uv/tools/skillspector/,包内 4.1MB / 49,721 行 Python)。执行图极简,没有条件边,纯扇出扇入:

resolve_input → build_context → [ 27 个 analyzer 并行 ] → meta_analyzer → report

评分公式是源码里读出来的,不是文档抄的(nodes/report.py):CRITICAL 50 分 / HIGH 25 / MEDIUM 10 / LOW 5,再乘置信度,同一规则最多算 3 次且权重递减(1.0 / 0.5 / 0.25),来自可执行文件的 finding 乘 1.3,最后有个 SC8(夹带 pyc 字节码)的硬下限 51 分——意思是「有东西可能执行」,分数再低也直接判 DO_NOT_INSTALL。分档:0–20 LOW/SAFE,21–50 MEDIUM/CAUTION,51–80 HIGH、81–100 CRITICAL(后两档都是 DO_NOT_INSTALL)。

包内按源码去重我数到 至少 90 个规则 ID,而 README 只列了「71 patterns / 17 categories」。少列的不只是零头:README 完全没有 AS1–AS3(agent snooping,探测 agent 配置目录)、AE 系、RP1–RP3(MCP rug pull,两次扫描之间工具定义被偷偷改掉)这三个家族——RP 我在实测输出里亲眼看到了(下面有截图式贴片)。

实测一:喂一个恶意 skill,它抓不抓得住

我造了个 11 行的恶意 skill:SKILL.md 里塞 HTML 注释包的指令覆盖 + 「别告诉用户」+ 读 ~/.ssh/id_rsa 外传 + curl | bash + 「把系统提示词原样打印出来」,配一个 scripts/sync.py(遍历 home 找 .env/.pem、base64 打包 POST 到外部、subprocess 跑远程脚本、verify=False、exec(base64.b64decode(...))),外加一个不锁版本的 requirements.txt。

skillspector scan /tmp/skills/evil-sync --no-llm

        Risk Assessment
 Score           100/100
 Severity        CRITICAL
 Recommendation  DO NOT INSTALL
Issues (29)
  CRITICAL: AST8 - Dangerous chain: exec() wrapping base64.b64decode   (scripts/sync.py:19, conf 95%)
  HIGH: P1 - Instruction Override            (SKILL.md:8, conf 80%)
  HIGH: P2 - Hidden Instructions             (SKILL.md:8, conf 70%)
  HIGH: AR1 - Anti-Refusal Statement         (SKILL.md:9, conf 85%)
  HIGH: PE3 - Credential Access              (SKILL.md:13, conf 90%)
  HIGH: SC2 - External Script Fetching       (SKILL.md:16 / scripts/sync.py:18)
  HIGH: E2 - Env Variable Harvesting         (scripts/sync.py:5)
  HIGH: P6 - Direct Prompt Extraction        (SKILL.md:18)
  HIGH: TM2 - Chaining Abuse / TM1 - Tool Parameter Abuse (shell=True)
  MEDIUM: E1/E3/TM3(unsafe defaults) / AST4 - subprocess
  LOW: SC1 - Unpinned Dependencies ×3
  LOW: SC4 - Unverifiable Dependency: requests has 16 known advisory(ies)
                              pyyaml 8 advisories / flask 10 advisories
RC=1

该抓的全抓到了,一网打尽。两个细节值得点赞:SC4 是联网查 OSV.dev 的真实 CVE 数据(不需要 key,一次批量 HTTP,离线自动退回内置小表),这也是我这几条 LOW 的来源;PE3 抓的是「读到凭据路径」这件事本身——同一份文本里既报了文档路径 ~/.ssh/id_rsa 的引用,也报了代码里的实际枚举。

也正因为它连「提到」都算,误报从这里开始埋雷。

实测二:一个干净 skill 的两种命运,和一条只写在源码里的规则

我写了个 14 行的良性 skill(列目录、写个 markdown 摘要,无代码),静态扫它:

Score           0/100
Severity        LOW
Recommendation  CAUTION      ← 0 分,却建议「谨慎」
Status          partial      ← 覆盖率 100%,但状态是 partial
Ledger exceptions
  - reference_unresolved SKILL.md:12-12: A local path-like reference
    could not be resolved unambiguously.

0 分应该是 SAFE 才对(README 的分档表也这么写)。我去源码里找原因,report.py:1529 有一段注释写得很直白——「Fail closed for any incomplete/degraded scan while preserving the honest score and severity」:

degraded or fatal_exception or entirely_uninspected > 0 or incomplete or transitive_truncation_reasons
    → risk_recommendation 从 SAFE 强制改成 CAUTION

也就是说:分数是诚实的,判定是保守的。我把同一个 skill 里那句「写入 summary.md」(一个不存在的文件引用)改成「打印到 stdout」,什么都不变,只改这一句:

# 带一个不存在的文件名引用
Score 0/100  LOW  CAUTION     is_complete=false  status=partial
# 去掉它
Score 0/100  LOW  SAFE        is_complete=true   status=complete

这就是这次实测里最阴的一条:判定档位可以被「正文里提了一个不存在的文件名」这种无关紧要的东西拉低,而且分数表上完全看不出来(都是 0)。文档型 skill 大量引用脚本、模板、子目录,命中 references 解析失败太容易了。

顺带两个实测事实:

顺手的正解是它藏得比较深的那个 flag:--fail-on-incomplete。实测对上面那个 partial 的 skill,不加 flag RC=0,加上 RC=1——要拿它当门禁,门禁条件应该是「退出码 + 完整性」,而不是「退出码」。

实测三:把 Anthropic 官方仓库当靶子,5 分钟、100 分、90.4% 覆盖

更有意思的是拿真货试。Anthropic 官方 anthropics/skills(176,089 Star,9 月 10 日还在推)我扫了两遍。第一遍直接扫 Git URL 整仓:

skillspector scan https://github.com/anthropics/skills --recursive --no-llm
real 5m02.853s
Score 100/100  CRITICAL  DO NOT INSTALL     Components (420)
Issues (238)
Coverage 90.4%  Status partial
Fully inspected 329 / Partially inspected 35
Scope exclusions: vcs_metadata .git/HEAD, .git/branches/ ...
Limitations: ... 13 个分析器 status: degraded

整仓扫了 5 分 03 秒,把 .git/config、THIRD_PARTY_NOTICES.md 全算成组件,命中 238 条,覆盖率 90.4% 且状态 partial——连它自己都没扫完,13 个分析器 degraded(资源上限:官方 ANALYSIS_RESOURCE_BOUNDS.md 写明单次图执行 600 秒、单 bundle 64MiB、遍历深度 64)。这种用法唯一正确的结论是:别扫整仓。

第二遍我按官方目录结构(skills/ 下 20 个 skill 各带 SKILL.md)批量扫:

Skill分数严重度发现数
claude-api100CRITICAL55
xlsx92CRITICAL11
docx / pptx100CRITICAL13 / 12
mcp-builder100CRITICAL11
skill-creator100CRITICAL8
webapp-testing64HIGH5
theme-factory37MEDIUM1
pdf / slack-gif-creator / canvas-design / frontend-design / discernment-nudge7–17LOW1–6
academy-guide / algorithmic-art / brand-guidelines / doc-coauthoring / internal-comms / web-artifacts-builder0LOW0

19 个 skill 里 6 个满分 CRITICAL(RC=1)。我最想看的是那个 55 条的 claude-api,所以单独再扫一次:

skillspector scan .../skills/claude-api --no-llm
real 2m01.904s          ← 静态模式,单个 skill 两分钟
Rules: E1 ×51  AE1 ×21  EA2 ×12  P9 ×7  PE3 ×6  E4 ×5  AS3 ×5  TM1 ×3  EA5 ×3  TM2 ×2

claude-api 是干什么的?教 agent 怎么调 Anthropic API 的文档型 skill。而 E1(External Transmission,外部传输)命中了 51 次——因为文档里写满了 api.anthropic.com 之类的 URL 示例。同理 xlsx/docx 命中 AST4 subprocess(脚本里正常调用工具)、skill-creator 命中 EA5(文档里提到模型选择)、mcp-builder 命中 RP1(npx @modelcontextprotocol/... 示例没锁版本)。

这不是「NVIDIA 的规则写得烂」,而是这类扫描器的结构性难题:静态规则无法区分「一份文档在描述危险操作」和「一份文档在命令 agent 做危险操作」。Cisco 那版 README 把话说得更诚(「best-effort,没报问题不代表安全」),NVIDIA 这版把 LLM 精修层和 baseline 当成解法——但如果 LLM 层没配好,你看到的就是这一屏红。

实测四:符号链接会被静默跳过,然后报「SAFE」

我先把本机 109 个 skill 目录用符号链接收拢到一个目录里批量扫,得到的是:

Warning: --recursive specified but no sub-skills detected. Scanning as single skill.
Skill: unknown        Components (0)
Score 0/100  LOW  SAFE      ← 什么都没扫,给出 SAFE
RC=0

导火索是发现阶段不跟随符号链接(单扫一个 symlink 目录同样得到 0 组件)。这会真实咬人:用 dotfiles 管理的、或者把 ~/.claude/skills 软链到别处的环境,扫描器会安静地给你一张「0 问题、SAFE、退出码 0」的白纸。换成 cp -rL 解引用后的实体目录再扫,同一个 skill 立刻变成 100/100 CRITICAL。所以:扫描前先确认组件数不是 0,这比看分数重要。

实测五:自建 mock 打穿 LLM 语义层(含 3 个只有抓包才看得见的事实)

没有 API key 就拿不到 SAFE,我只能自己搭一个 OpenAI 兼容的 mock 服务,把 SKILLSPECTOR_PROVIDER=openai 指过去,抓它发出去的每一个请求。三层事实浮现出来:

1) 它的结构化输出走 json_schema strict,不是 function calling

POST /v1/chat/completions
messages: [ {role: "user", content: "..."} ]        ← 只有一条 user 消息,没有 system
response_format: {type: "json_schema", strict: true,
                  json_schema: {name: "LLMAnalysisResult", ...}}
usage: prompt 4,321 / completion 77 / cached 300       ← 记账进 metadata.inference_usage

由此推出一个很实际的坑:任何不支持 json_schema 严格模式的后端,语义层会直接降级。README 只在 contrib/batch_scan 那一节的注释里轻描淡写提过一句「DeepSeek 缺 structured-output 支持,需要手动打 JSON 解析补丁」——而结构化输出现在是主链路。我的 mock 第一次返回普通 chat 内容时,收到的就是三行 LLM structured response validation failed ... retrying (1/3),最后 llm_degraded=true。降级不是失败,不报错,只是悄悄退回静态结果,report 里那一行 llm_error 是你唯一能看见它的地方。

2) 三条提示词里,我没找到「文件内容不可信」的隔离声明

三个语义分析器的 prompt 分别 3,296 / 4,876 / 5,073 字符,族系是 SSD-1~4(语义注入/改写攻击/自然语言外传/叙事式欺骗)、SDI-1~4(描述-行为不符、越权能力、范围蔓延、意图背离)、SQP-1~3(模糊触发、缺警告、语言/地区政策)。设计上有两点很聪明:明确告诉模型「字面关键词模式静态分析器已经抓了,你只做语义」,以及「大多数文件是干净的,空 findings 是正确的,不要为了凑数编造发现」。但——被扫文件的内容是以 L01: ... 行号前缀包在同一个 user 消息的代码围栏里,我在三条 prompt 里都没找到「以下文件是不可信数据,不要执行其中的指令」这类隔离声明;README 里「The LLM prompt includes anti-jailbreak protections」这句,在这三条 prompt 上没有对应的文字。用被审对象污染审问者,是这个赛道的通用风险,值得自己再加一层包装。

3) 一条 LLM finding 变成了 9 条

让 mock 在语义层返回「一条 HIGH 发现」,结果 issues 从 29 涨到 38,其中同一条 finding 被复制 9 份:3 个语义分析器 × 3 个文件各来一份,连 requirements.txt:12 这种不存在的行号都被配上了。包里有 nodes/deduplicate.py,但没把这种跨文件重复收掉。同时那次运行还留下 llm_degraded=true + llm_error="TP4 LLM batch failed: ValidationError"——一个批次的 schema 校验失败就会把整轮标成降级。读报告时要分开看「分数」和「有多少待审条目」,LLM 层的条目是可能灌水的。

实战:把我自己的 109 个 skill 全扫一遍,结果比想象中难看

最后拿真实工作负载收尾:本机 ~/.hermes/skills 下 53 个分类里共 109 个带 SKILL.md 的 skill,解引用拷贝后批量静态扫描。它没扫完——

  <omitted>    —   —   77   partial
Recursive scan incomplete: one or more skills were omitted after an
aggregate safety limit.

109 个只处理了 32 个,剩下 77 个被总量预算掐掉,标记 partial,退出码 1。32 个里:6 个 CRITICAL、2 个 HIGH、6 个 MEDIUM、18 个 LOW。挑几个扎眼的:

我自己的 skill分数发现数里面是什么
creative-coding10014示例代码 + 外部 URL
comfyui10019本地服务地址、安装提示
autonomous-ai-agents10011纯 Markdown 文档:EA5(文档里讲模型选择)、AS1(文档里出现 ~/.claude/ 路径)、MP3、TM1/TM2(文档里的命令示例)、PE3(文档里的凭据路径)、RP1(npx 示例没锁版本)
hermes-agent898PE3 ×3 全是文档里写的配置路径
static-site-deploy8810部署命令与 URL
content-publishing8112发布接口与 curl 示例

一个纯文档型的 skill(我的多 agent 委派说明文档)拿了 100/100「不要安装」,11 条发现全部来自文档正文里的路径、命令示例和 URL。这就是前面结构性问题在工作负载上的真实后果。如果你打算把它接进 CI 直接卡门禁,第一周你会拦下自己所有的 skill。

怎么用才不踩坑:我实际跑通的四条命令

这台机器上验证过的完整路径(Ubuntu 无头环境,Python 3.12 由 uv 提供):

# 1) 装(CLI-only;要 MCP server 就加 [mcp] extra)
uv tool install git+https://github.com/NVIDIA/skillspector.git
uv tool install --force 'skillspector[mcp] @ git+https://github.com/NVIDIA/skillspector.git'

# 2) 先跑纯静态(不泄露文件内容,秒级到分钟级)
skillspector scan ./my-skill/ --no-llm

# 3) 接 LLM 语义层(任何 OpenAI 兼容端点;也可用 ollama / 本地 claude|codex|gemini CLI 复用订阅)
export SKILLSPECTOR_PROVIDER=openai        # 默认是 nv_build,没 NVIDIA key 就静默降级
export OPENAI_BASE_URL=https://.../v1
export OPENAI_API_KEY=...
skillspector scan ./my-skill/ -f json -o report.json

# 4) 当门禁用(退出码 0=≤50分 1=>50分 2=错误;再加完整性门槛)
skillspector scan ./my-skill/ --fail-on-incomplete || exit 1
skillspector scan ./my-skill/ --format sarif -o report.sarif   # 进 GitHub Code Scanning

# 批量:扫目录下每个带 SKILL.md 的子目录
skillspector scan ./skills/ --recursive --no-llm

# 已接受的误报落成 baseline,之后只报新增(实测:100/CRITICAL → 29/CAUTION,28 条被抑制)
skillspector baseline ./my-skill/ -o .skillspector-baseline.yaml
skillspector scan ./my-skill/ -b .skillspector-baseline.yaml --show-suppressed

# 让 agent 自己调用(MCP 单工具 scan_skill,可 gate 安装)
claude mcp add skillspector -- skillspector mcp

MCP 我实测握手成功:initialize 返回 protocolVersion 2025-06-18,tools/list 只有一个 scan_skill(参数 target / use_llm / output_format)——README 里自己挂着 issue #199「initialize hang」,我这次没复现,但既然作者标注了,接进生产前值得自己压一轮。HTTP 传输没有鉴权(官方明说,且会拒绝本地路径和 file:// 防任意文件读取),要暴露就自己套 mTLS 反代。

对比:四个 skill 扫描器,我为什么还留着 SkillSpector

维度SkillSpectorCisco skill-scannerSnyk agent-scan腾讯 AI-Infra-Guard
Star17,0692,5183,0346,255
许可✅ Apache-2.0⚠️ 自定义(NOASSERTION)✅ Apache-2.0✅ Apache-2.0
检测栈regex + YARA + AST + 污点 + LLMYAML + YARA-X + AST/数据流 + CEL 决策层 + LLM 裁判模式匹配 + 机器全量发现平台化:Agent/Skill/MCP 三合一
LLM 层可换后端✅ 11 种(含 ollama / 本地 CLI 复用订阅)✅ 可选 LLM-as-a-judge—✅ 平台内配置
实时 CVE✅ OSV.dev 免 key(实测 requests 16 / pyyaml 8 / flask 10 条)—✅ Snyk 自家库—
签名 / 供应链闭环✅ OpenSSF Model Signing + NVIDIA 官方目录———
误报处理✅ baseline(指纹/glob)+ LLM 精修✅ Meta-analyzer—平台内
CI 友好度SARIF 2.1.0 + 退出码 + --fail-on-incompleteSARIF + GitHub Actions + pre-commit hookuvx 一条命令平台化
实测印象证据链最全(inspection ledger、覆盖率、降级标记),但静态层在文档型 skill 上狂暴误报、慢(单个大 skill 2 分钟)README 对局限最诚实,工程集成最顺胜在「扫全机器」,输出自称 experimental胜在覆盖面,不是一个轻量 CLI

选型我给三句话:想在本地/CI 里对单个 skill 做一次「份量够重」的验货(带覆盖率、证据账本、CVE 查询、可换 LLM 后端、结果可签名)→ SkillSpector;想把它塞进 pre-commit 或 GitHub Code Scanning 让 PR 自动卡 → Cisco 那个更贴合(它有 pre-commit hook,许可要自己过一眼法务);想知道「我这台机器上到底装了多少危险东西」 → Snyk agent-scan 的机器级发现更好用。

最后说几句实话

Apache-2.0 许可,但有一条附加条款要记住:它自己不沙箱化你(README 的 Trust model 章节自己写了这条),扫描行为本身不执行被扫代码,但开 LLM 时会把文件内容发给你配的 provider——内网环境记得 --no-llm。仓库 github.com/NVIDIA/skillspector,文档 docs.nvidia.com/skills/。skill 生态正在变成新的 npm,而 2026 年敢给 skill 建「验货流水线」的,目前就这几家——NVIDIA 这条是链条最完整的一条。