SKILL.md frontmatter 字段全集:哪些必填
你要给某个软件补一份 SKILL.md,或者想判断仓库里现成的某一份是不是写齐了,第一个问题都是同一个:frontmatter 到底该填哪些字段,哪些是必填。
这个问题在 CLI-Anything 里有一个不太舒服的答案:「必填」在这个仓库里同时由三处东西约定,而这三处谁也没有能力拦住你不填。下面先把字段全集摆出来,再说这三处分别是什么。
先看字段全集
我们采集时(2026-08-10,对应仓库快照 39634a6)skills/ 目录下共 70 个子目录,每个子目录恰好只有一个 SKILL.md。把这 70 个文件的 frontmatter 逐行取顶层 key 数一遍,出现过的 key 一共 17 个:
| 字段 | 出现次数(分母 70) |
|---|---|
name | 70 |
description | 70 |
version | 8 |
install | 4 |
requires | 4 |
command | 3 |
categories | 3 |
category | 2 |
tags | 2 |
author | 1 |
commands | 1 |
metadata | 1 |
display_name | 1 |
entry_point | 1 |
binary | 1 |
contributor | 1 |
entrypoint | 1 |
这张表读起来很快:只有 name 和 description 是 100% 覆盖的,其余 15 个字段全部落在个位数。带额外字段的文件总共只有 10 个:firefly-iii、jumpserver、live2d、lldb、musescore、nsight-graphics、nslogger、openrefine、renderdoc、wiremock。也就是说 70 份里有 60 份的 frontmatter 就是干净的两个字段——一个 name,一个 description。
到这一步,「哪些必填」看起来已经回答完了。但把「100% 覆盖」直接读成「必填」,会漏掉这个仓库里最要紧的一层。
「必填」是三样不同的东西
在这个仓库里,说 name 和 description 必填,其实是三个各自独立的依据叠在一起:
第一,生成模板只产出这两个字段。 cli-anything-plugin/templates/SKILL.md.template 的第 1 到 6 行就是 frontmatter 块,里面只有 name 和 description 两个占位符。skills/ 里能看到明显的模板痕迹:我们采集时 23/70 的 description 形如 Command-line interface for <显示名> - <README 首段摘要>,对应 cli-anything-plugin/skill_generator.py 第 136 到 141 行取 README 首段前 100 字符的逻辑;同时完整具备模板那 9 个固定小节的文件是 14/70,一个模板小节都不含的有 10/70。凡是走生成器出来的,字段集合天然就是这两个。
第二,生成指南把它写成了规定。 cli-anything-plugin/guides/skill-generation.md 第 17 到 24 行明确规定 frontmatter 只含 name 和 description。这是文档层面的口径。
第三,覆盖率是 70/70。 这是我们逐个文件数出来的统计事实。
三条依据指向同一个结论,但它们的性质完全不同:一条是工具的默认输出,一条是文档的规定,一条是当下的仓库状态。前两条不会阻止任何人手写一份带十个字段的 SKILL.md,第三条则会随着下一个 PR 变化。
反直觉的那一处:没有校验字段集合的东西
顺着上面往下问一句:那有没有哪个脚本会检查「你这份 SKILL.md 的 frontmatter 是不是只有 name 和 description」?
没有。
.github/workflows/ 下我们采集时共 6 个 workflow:check-codex-skill.yml、check-root-skills.yml、deploy-pages.yml、pr-labeler-tests.yml、pr-labeler.yml、publish-cli-hub.yml。其中只有 check-root-skills.yml 与 skills/ 有关,这个 workflow 一共 35 行,唯一一个步骤就是跑 python3 .github/scripts/validate_root_skills.py。
而这个校验脚本(114 行)校验的是什么?它的文档字符串写得很直白:Validate that deep harness SKILL.md files are mirrored in repo-root skills/.——它校验的是「镜像有没有同步」,分成两类结果:clobber(镜像里有源里没有的行,重跑同步会被删掉,必须回源改)与 stale(源领先于镜像,重跑同步即可修复),逻辑在第 44 到 71 行。clobber 的报错文案里有一句原文是 You edited a GENERATED file.
也就是说,CI 关心的是两份副本是否一致,不关心 frontmatter 里有几个字段、字段叫什么名字。除了这一处,我们没有在仓库里发现任何针对 SKILL.md 的 lint 或 markdown 规则脚本;顶层也没有 scripts/ 目录。
真正会被机器动过的 frontmatter 字段只有一个——name。.github/scripts/sync_root_skills.py(93 行)在把 packaged 副本写进 skills/ 之前,会把 frontmatter 里第一个 name:(连同它后面的缩进续行)整体改写成 name: "<skill_id>",如果原本没有 name 就插到最前面(第 26 到 55 行)。skill id 的规范化规则在第 13 到 23 行:优先取 cli_anything 后一级的包名,下划线转连字符,加 cli-anything- 前缀。
所以字段的”强制”程度其实是分层的:name 是唯一一个会被脚本动过的字段——但被脚本改的只是写进 skills/ 的那份镜像,源文件里写错了仍然留着(仓库里就有 5 份 packaged 副本的 name 与 repo-root 的 skill id 至今对不上);description 全靠模板与指南的约定;其余 15 个字段则处在完全自由的区域。
自由区域长出了什么
没有 schema 约束的直接后果,在那 10 个带额外字段的文件里看得很清楚:同一件事出现了不同拼写,而且两种拼写并存。
entry_point(skills/cli-anything-musescore/SKILL.md:6)与entrypoint(skills/cli-anything-wiremock/SKILL.md)category(skills/cli-anything-musescore/SKILL.md:7)与categories(skills/cli-anything-lldb/SKILL.md:11)command(skills/cli-anything-lldb/SKILL.md:5)与commands(skills/cli-anything-jumpserver/SKILL.md,值是嵌套的 group / subcommands 结构)
回头看上面那张字段表,这三对拼写分裂正好解释了表里为什么会有 categories 3 次而 category 2 次、command 3 次而 commands 1 次这种看着别扭的分布——它们不是两种不同语义,是两种写法。
还有一个只出现过一次、写法也很特别的字段:metadata,在 skills/cli-anything-live2d/SKILL.md:4,值是一段 inline JSON:
metadata: { "cli-anything": { "category": "graphics", "requires": null } }
这一条把 category 和 requires 又嵌了一层,跟前面那两种写法都不一样。
另外,skills/ 之外还出现过一个本目录里没有的字段:reasonix-skill/SKILL.md:4 的 runAs: subagent。
这几处差异只是可以各自核对的事实,我们陈述到这里为止。
你自己核一遍的动作
上面每个数都能自己数一遍。目录数与文件数用这几条:
find skills -maxdepth 1 -mindepth 1 -type d | wc -l
find skills -maxdepth 1 -mindepth 1 -type d -name 'cli-anything-*' | wc -l
find skills -name 'SKILL.md' | wc -l
我们采集时依次得到 70、69、70。70 减 69 等于 1,多出来的那个不叫 cli-anything-*,是 skills/cli-hub-meta-skill/,它不对应任何一个软件的 harness。
字段统计这一步有个坑要提前说:不要直接 grep '^name:' 去取值。这个坑在 skills/ 这 70 份里其实碰不到——它们的 name 全是单行写法(下一段会说这 70 行的具体形态);但你要是去数 cli_anything/<software>/skills/SKILL.md 那一侧,就会踩到:生成模板输出的 name 是 YAML 折叠标量,形如 name: >- 后换行再缩进写值(templates/SKILL.md.template:2-3),你 grep 到的那一行右边是空的,真正的值在下一行的缩进里。同步脚本之所以要专门处理「name: 及其后的缩进续行」,就是为了这个形态。稳妥的做法是把 --- 之间的块整段交给 YAML 解析器,我们数这 17 个 key 用的就是这个办法。
顺带一个同源的细节:70 个文件里有 69 个的 name 值是用双引号包着的(name: "cli-anything-x"),只有 1 个没加引号——skills/cli-hub-meta-skill/SKILL.md:2 的 name: cli-hub-meta-skill。至于同一份内容在仓库里落了几份、两份之间差在哪,另有一篇专门讲副本一致性,这里不展开。
还有一个可以顺手核的:70 个文件的 name 值与所在目录名是不是对得上。我们逐个比对过,mismatch 为 0。
字段齐全,不等于这个 harness 你装上就能用
最后必须把这条说清楚,否则前面那张表容易被读成一张「合规清单」。
frontmatter 写齐了 name 和 description,只说明这份 Markdown 的头部符合模板口径,它跟「对应的软件能不能被驱动起来」是两件事。仓库里现成的反例就有:harness 目录 sketch 没有任何 SKILL.md——find sketch -iname '*skill*' 没有输出,registry.json 里 sketch 的 skill_md 字段是 null。registry.json 里 skill_md 为 null 的一共 5 条:adguardhome、comfyui、mermaid、sketch、clibrowser。这里要分清两件事:其中 adguardhome、comfyui、mermaid 在 skills/ 下其实是有文件的,只是注册表这一栏没填;真正一份 SKILL.md 都找不到的是 sketch。「注册表字段缺失」和「文件缺件」不是一回事。
而且这类 harness 的性质本身就要求你先看清边界:它们会在你本机执行外部程序与脚本,包装真实桌面软件的那些还需要你自行安装上游应用(根 README 第 236 行专门提醒了这一点)。仓库自己也在若干处留了余地,原样抄给你看:skills/cli-anything-notebooklm/SKILL.md:69 写的是 Treat this harness as experimental and unofficial.;skills/cli-anything-zotero/SKILL.md:29 写的是 Experimental SQLite write commands are local-only, user-library-only, and should be treated as non-stable power-user operations.
所以回到最初那个问题:你自己写一份 SKILL.md,frontmatter 填什么?按模板与生成指南的口径,填 name 与 description 两个就够,name 用 cli-anything-<软件名>、下划线转连字符(这也是同步脚本会给你改成的形态)。要不要加 version、requires、category 这些,项目没有给出通用规定——仓库里只有 10 个文件加了,加的方式还各不相同,加了也不会有脚本来管你。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。