审核技能不能只看 SKILL.md:用 dsh-skills-anywhere 的目录清单核对脚本和参考文件

SKILL.md 没变,脚本可能已经变了。dsh-skills-anywhere 0.9 的目录清单记录每个文件的路径、长度和 SHA-256:比较退出码 0、1、2,MCP 加载时按摘要拒绝改动,以及它证明不了什么。

对比图:标题下三个不带编号的实心圆并排,每个圆下有一条横线,下面分别写着:expected_sha256 只核对 SKILL.md,其他文件改了也不会发现;expected_bundle_sha256 重算整个目录清单,文件增、删、改即报错,不返回指令;include_bundle: true 返回当前清单供查看,不与审核值比较。底部是来源说明。
MCP 工具 open_skill 的三个相关选项各自核对什么,依据 noteflowai/dsh-skills-anywhere commit c011051 的 docs/BUNDLES.md 绘制;三者是并列的选项,可以组合使用,传入 expected_bundle_sha256 即开启 include_bundle;排列顺序不代表优先级。

dsh-skills-anywhere 0.9 在原有的单文件检查之外,加了一份有上限的技能目录清单(bundle manifest)。文档开头写明了原因:SKILL.md 没变,不代表它旁边的脚本和参考文件也没变。本文依据固定在 commit c011051 的 docs/BUNDLES.md 和 docs/MCP-COMPATIBILITY.md,讲怎样把这份清单用到技能审核里:生成和比较清单的命令,三个退出码,怎样在 MCP 加载时要求同一个目录身份,清单的格式与上限,以及摘要匹配证明不了什么。

本站的 Skills Anywhere:把各家 AI 工具里的 SKILL.md 汇成一个目录,经 dsh 或 MCP 按需加载 概述过这个项目及其文件检查与按哈希加载。下文描述的行为都来自项目文档([O],项目自述)。文档里的测试结果由项目报告,本站没有运行这些命令或测试。

为什么只核对 SKILL.md 不够

文档引用的 Agent Skills 规范(文档注明核对日期为 2026-09-14)把一个技能描述为 SKILL.md 加上可选的 scripts、references 和 assets。MCP 工具 open_skill 原有的 expected_sha256 只核对 SKILL.md。MCP-COMPATIBILITY.md 写明,这个哈希不覆盖被引用的脚本或其他文件,也不能确认作者身份。所以可能出现这种情况:审核时读过的 SKILL.md 一字未改,它让 agent 运行的脚本却已经换了。

目录清单记录所选目录下的每一个普通文件(regular file):相对路径、字节长度和 SHA-256,隐藏文件和二进制文件也算在内。文档还说明,这是项目自定的内容身份格式,并不声称属于 Agent Skills 规范。

在本地和 CI 里生成、比较清单

文档给出的两条命令如下,注释按原文保留:

# Keep both the review manifest and output logs outside the skill directory.
npx -y dsh-skills-anywhere@0.9.0 bundle /path/to/skill --json > reviewed-bundle.json
# Review the actual files, then retain the manifest in your review record.
npx -y dsh-skills-anywhere@0.9.0 bundle /path/to/skill \
  --against reviewed-bundle.json --json

第一条注释的意思是:审核清单和输出日志都放在技能目录之外;第二条是:审核实际文件,然后把清单保留在审核记录里。第一条命令为审核时的目录生成清单;第二条用 --against 把当前目录与保存的清单比较。命令里写了包版本 @0.9.0,所以 npx 运行的工具版本由命令本身固定。这一节的标题是“Local review and CI”,但文档没有给出 CI 配置文件。

命令退出码文档写明的条件
生成清单0生成成功
比较(--against)0文件路径和内容一致
比较(--against)1有差异
比较(--against)2输入无效、读取不稳定,或目录中有不支持的条目

文档没有写明生成清单失败时的退出码,本文不作推测。比较的 JSON 输出包含 added、removed、changed、两边的摘要和当前的 manifest。重命名表现为一个删除路径加一个新增路径。面向人的输出会对来自输入的路径做转义。两条命令都不会去发现其他技能根目录,不运行 Git,不同步仓库,也不执行技能代码。

清单放在哪里

保存的清单必须放在技能目录之外,否则它会变成自己的输入。shell 重定向可能在命令启动之前就创建好文件,所以要始终重定向到单独的审核目录。上面的命令把 reviewed-bundle.json 写在当前目录,也就是说,运行时当前目录不能位于技能目录之内。工具不会悄悄忽略任何文件:应传入技能自己的目录,而不是带缓存或 Git 元数据的整个应用检出。

一个假设的例子

下面是本站为说明而设的情形,不是运行结果。假设审核时目录里只有 SKILL.md 和 scripts/review.py,之后只有 scripts/review.py 被改过。按文档的约定,第二条命令以退出码 1 结束,scripts/review.py 出现在 changed 中。如果改动是把它改名为 scripts/check.py,结果会是 removed 里一个 scripts/review.py,added 里一个 scripts/check.py。如果目录里多了一个指向别处的符号链接,文档把它列为被拒绝的条目,按上表应得到退出码 2,而不是 1。

在浏览器里比较收到的清单

Hugging Face 上的演示页带一个作者编写的示例,其中只有 scripts/review.py 发生了变化;示例的两份清单在站点构建时由真实的文件系统读取器生成。可以下载其中任一份清单、恢复与之对应的文件,也可以打开自己的两份 JSON 清单查看差异。比较之前,浏览器会用与 CLI 相同的规范化方式重新计算每份清单的摘要。输入只留在页面内存中,不上传、不存储,也不写进分享链接;输入无效时清除对应结果;重置为示例时取消尚未完成的文件读取。这个页面比较的是已记录的清单,不能检查你的文件系统,也不能连接本地 MCP 服务器。

在 MCP 加载时要求同一个目录身份

审核通过后,调用 open_skill 时带上清单里的摘要:

{
  "name": "your-discovered-skill-name",
  "expected_bundle_sha256": "<manifest.sha256 from your reviewed bundle>"
}
  • open_skill 在返回指令之前重新计算目录清单。只要有文件被新增、删除或修改,摘要就对不上,调用返回错误,不带指令正文。返回的 bundle 字段是这次核对用的清单。
  • 没有事先保存的摘要时,可以用 include_bundle: true 查看当前清单。传了 expected_bundle_sha256 就隐含这个选项。
  • expected_sha256 仍然只核对 SKILL.md,两个摘要参数可以同时使用。在目录模式下,返回的指令和它的文件哈希,与目录清单用的是同一份字节。
  • 即使摘要匹配,作者当前设置的禁用选项仍然生效。
  • 目录哈希只在独立 MCP 服务器的 open_skill 工具上按需开启;默认的工具和资源加载,以及 dsh provider,行为不变。

MCP-COMPATIBILITY.md 报告了 0.9 的相关测试:同样的四种 SDK 配置(SDK 1.30.0,以及 SDK 2.0.0 的 legacy、auto 和固定 2026-07-28 三种协商方式)都用 expected_bundle_sha256 加载一次,然后只修改一个资源文件、保持 SKILL.md 不变,确认加载被拒绝。已安装的 npm 包也用两代 SDK 做了这项检查,CLI 操作在空的 PATH 下运行。这些是项目自己的测试记录。

清单格式与上限

格式 skills-anywhere-bundle-1 恰好有四个字段:schema、sha256、total_bytes 和 files;每个文件条目恰好有 path、bytes 和 sha256。各行按斜杠分隔的相对路径排序,用 JavaScript 字符串升序;路径不重复,一个路径也不能同时是另一个文件的父路径。不做 Unicode 规范化,也不用依赖区域设置的排序。目录摘要是下面这个 UTF-8 JSON 元组(不含空白)的 SHA-256:

["skills-anywhere-bundle-1",[["relative/path",byte_length,"file_sha256"],...]]

目录的绝对路径、包版本、时间戳和 JSON 排版都不属于这个身份,所以把相同的文件移到另一个目录,摘要不变。SKILL.md 必须存在且是合法的 UTF-8,但生成清单时不校验 frontmatter,解析结果要用 check 命令看;其他文件可以是二进制。

上限项上限
文件数512
目录条目总数1,024
路径层级12
SKILL.md128 KiB
其他单个文件16 MiB
目录总量32 MiB
收到的单份清单1 MiB
单个相对路径1,024 个 UTF-8 字节

路径中含控制字符、反斜杠、冒号、路径穿越段或 Unicode 替换字符时一律拒绝。如果安装根目录本身是符号链接,只解析一次;根目录之下的符号链接,以及管道这类特殊文件,都会被拒绝。文件读取、目录枚举和最后的元数据检查,都会发现检查过程中发生的变化。

局限 / 注意

  • 摘要匹配只说明记录下来的文件路径和内容字节相同。它不认证作者,不判断代码是否安全,也不证明模型按指令做了。
  • 空目录、权限、所有者、目录外的文件、已安装的包、解释器版本、远程 URL 和动态加载的内容,都不在摘要范围内。
  • 它不推断也不认证 Git 修订版本。仓库来源重要时,要另外保留一个审核过的源码修订版本。
  • 检查不会冻结目录,之后的资源读取或执行可能读到不同的内容。文档建议使用稳定、受控的检出,并在实际使用的地方执行自己的执行策略。这项功能只在加载或检查时发现目录变化,不是沙箱,也不是防御能力的基准测试。
  • 默认的工具和资源加载不做目录哈希;不显式传参数就没有这项保护。
  • MCP 兼容测试检验的是 SDK 层面的协议互通,不认证 Claude Code、Cursor、Codex 等具体应用的每个版本。文档在 0.8 的说明中写明该包通过 stdio 提供服务,0.8 没有加入 HTTP 传输、授权或远程托管。
  • 本文依据 commit c011051 的文档,命令固定为 0.9.0;之后的版本在参数、字段或退出码上可能不同。

链接