mcpField
S
skill-authoring

skill-authoring

github.com/theodoreadAgent 工作流039 次安装4 周前更新
代码仓库

关于 skill-authoring

skill-authoring 是用于编写、维护、安装和验证 Agent Skills 的技能,重点说明如何部署变更,使 Agent 实际加载更新,包括确认源仓库、重新安装和重新加载。

主要功能

  1. 01技能格式与元数据说明
  2. 02以源仓库为准的编辑与发布流程
  3. 03安装、重载与验证步骤
  4. 04技能名称冲突检查

适用场景

  1. 01为共享技能仓库编写技能
  2. 02部署已安装技能的更新
  3. 03采用名称前检查技能重名

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/theodoread/agent-skills --skill skill-authoring

安装后在 Claude Code 中输入/skill-authoring确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
naming.md
rationale.md
scripts/
names.py

详细介绍

编写和更新 Agent Skills

技能是一个目录,根目录下有一个 SKILL.md:包含 YAML frontmatter 和 Markdown 指令。 references/ 存放按需读取的内容,scripts/ 存放技能运行的内容。格式就是这些,所有遵循 Agent Skills 约定的 Agent 都会读取它。

frontmatter 仅包含规范定义的 6 个键,不得有其他键:name 和 description,每个 Agent 都会据此进行匹配;license;compatibility,最多 500 个字符,用于说明环境要求——技能面向的产品、所需的系统软件包,以及是否会访问网络;metadata,一个字符串到字符串的映射,用于记录规范未定义的内容;以及实验性的 allowed-tools。任何包含脚本或指示在会话仓库之外写入内容的技能,都应使用 compatibility;纯约定类技能则不必填写——规范本身也指出,大多数技能都不需要它。metadata 下的键由你负责说明,因此应先在仓库的指令中记录相关决策;这里曾随手添加 metadata: family:,但没有任何内容读取它,因此已将其删除。

本技能涵盖如何编写技能,以及——实际最容易出错的部分——如何让对技能所做的更改生效。

本技能读取、运行和写入的内容

本技能附带一个脚本 scripts/names.py,用于读取技能目录并查询公共搜索端点。它接触的其他内容,都是通过它要求你运行的命令来操作的。

  • 读取:scripts/names.py 会读取你指定根目录下每个技能的 SKILL.md——默认是加载器自身的作用域 ~/.agents/skills/ 和项目的 .agents/skills/——只获取名称和文件摘要,不读取其他内容。不读取对话记录、进程或 Git 状态。
  • 运行:npx skills add、skills ls、skills remove;以及 skill-fitness 用于执行下述测量的脚本。names.py 不会调用任何外部命令。
  • 写入:skills add --global 会将技能复制到 ~/.agents/skills/,并将每个检测到的 Agent 自己的技能目录链接到该目录;不加 --global 时,如果当前位于某个仓库中,它会将 .agents/skills/、.claude/skills 符号链接和 skills-lock.json 写入该工作树——这就是此处不能省略该标志的原因。如果安装程序跳过了链接步骤,它建议运行的 ln -s ~/.agents/skills ~/.claude/skills 会创建一个符号链接。除此之外不会写入任何内容。
  • 网络:skills add <owner>/<repo> 会克隆你指定的仓库。names.py 会查询 skills.sh/api/search——每个名称发送一次 GET 请求,且只发送名称——除非使用 --offline,此时不会访问网络。

更新技能并重新部署

~/.agents/skills/<name>/(或某个 Agent 专属目录,或项目的 .agents/skills/)中的副本不是源文件。 它是在安装时创建的普通文件副本。编辑它看起来似乎有效,但之后会发生这些情况:下一次安装会覆盖它,其他项目或机器都看不到它,而且没有差异对比、没有提交,也没有对更改内容的审查。每次编辑技能,都应先找到源仓库。

  1. 找到源仓库。 其 README 或技能自带的页脚通常会注明源仓库。如果没有,skills ls 会显示已安装的内容及其来源,而该仓库可以像普通仓库一样通过 git clone 克隆。如果无法确认哪个仓库是技能的来源,请先询问,不要退而求其次去编辑已安装的副本。

  2. 编辑源文件——前提是当前会话位于其所属仓库中。 修改应以增补为主,并尽量精简——在需要细化的规则下添加一个要点,而不是重写。关于_原因_的说明应放在 references/ 中,而不是正文里(见下文“正文应包含什么”)。 如果当前会话在另一个仓库中工作,流程到此为止。 在不属于当前会话的检出目录中编辑并提交,会把提交留在别人的工作树里;在运行多个并行会话的机器上,这在 git log 中看起来再平常不过——拥有该仓库的会话可能会在不知道提交并非出自自己的情况下推送它。请将更改提交到该技能自己的仓库,并留给下一次在那里工作的会话应用。已于 2026-08-30 确认:某次运行从一个无关项目中对某项技能做了两处正确且通过质量门禁的修改,并将其提交到了那个项目中,这正是本条款由来的原因。

  3. 运行仓库的质量门禁。 Markdown 也不例外:会重排文本的格式化工具会改写 SKILL.md,而跳过质量门禁的纯文档更改,是最常见的导致 CI 变红、给别人添麻烦的原因。

  4. **提交之前先征求同意,**除非仓库自身的约定另有规定。技能是一项共享约定;更改技能,就是更改今后每个会话的行为方式。

  5. 推送。 这一步最容易被跳过,却至关重要:安装器从远程仓库克隆,所以,提交了但尚未推送的编辑对安装过程不可见。本地提交不会改变任何 Agent 加载的内容。

    [陷阱:推送是必要条件,但不是充分条件——skills add <owner>/<repo> 使用的是远程仓库的_默认分支_。 从功能分支推送会成功,重新安装也会成功,但不会安装任何新内容;接下来步骤 7 会将已安装副本与从未发布的检出目录进行比较,而且流程中的每条命令都会以 0 退出。请先合并,或者在迭代期间从本地路径安装(见下文)。

    关联工作树最容易让人在不知情的情况下踩中这个坑,因为工作树总是位于某个分支上——这正是工作树的用途——而工作树中没有任何提示会告诉你这一点。Claude Code 的 EnterWorktree 会在仓库内的 .claude/worktrees/<name> 下创建工作树;VS Code 的工作树支持默认会将工作树放在同级的 <repo>.worktrees/ 目录中,且没有可更改此设置的选项。session-harvest 的 skills-state 会打印一行 worktree:,显示工作树的位置;而 git rev-parse --git-common-dir 指向当前检出目录的 .git 之外,则是直接的判断方法。]

  6. 重新安装。 没有任何机制会监视源文件:

    npx skills add <owner>/<repo> --global --skill <name>     # one skill
    npx skills add <owner>/<repo> --global                    # the whole repo
    

    如果机器通过声明式方式安装技能(例如使用设置仓库或点文件管理器),请改用该机制,以确保其自身记录保持准确——底层仍会调用同一个 CLI。

    [陷阱:-g/--global 不是默认选项——不加此选项时,安装范围会根据你所在的位置静默决定,但无论哪种情况,摘要都会显示成功。 文档将此选项描述为“全局(用户级)安装技能,而非项目级”;如果省略,CLI 会在项目中时选择项目级,在项目外时选择全局。因而,在仓库内运行时,它会将 .agents/skills/、一个 .claude/skills 符号链接和 skills-lock.json 写入该工作树,而你本想更新的用户范围副本却仍然过时。最糟糕的是在技能仓库本身内运行:它会在仓库中放入供用户使用的技能副本,还会放入一个可能被坚持保持供应商中立的仓库从原则上拒绝的、特定于运行环境的目录;这三者都不会被 gitignore,因此下一次 git add 会把它们一并加入。 改在其他地方执行并不能解决问题;关键在于标志。 skills-lock.json 是一个_项目_产物——skills experimental_install 会从中恢复——因此,按当前工作目录确定范围的运行仍会在你执行命令的位置生成一个文件,其中只列出本次调用涉及的技能。已于 2026-08-30 确认,同一会话中验证了这两种情况:第一次从仓库目录运行,在仓库中安装,未更新任何全局内容;第二次从 $HOME 运行,安装位置正确,但在 ~/skills-lock.json 中留下了一条记录,列出了这台机器上十个技能中的一个;这个文件看起来像清单,其实并不是。传入 -g,结果就不再取决于当前工作目录。之后检查 git status,尤其是会话的当前工作目录是某个仓库时。]

  7. 验证,不要想当然。 skills ls -g --json 会列出每个已安装技能及能够看到它的 Agent。如果技能带有 scripts/ 目录,就从安装路径运行其中一个有文档说明的命令——列表只能说明技能已安装,不能说明其文件也已到位。 已于 2026-08-29 在线确认:某个技能的 SKILL.md(以及一条部署到始终加载的指令文件中的规则)要求每个会话都运行 python3 ~/.agents/skills/<name>/scripts/<file>,但它安装自一个早于 scripts/ 的提交,因此机器上并不存在文档所述的路径,尽管这两份文档都坚称路径存在。直到有人运行它,问题才会暴露。

    [PITFALL: 单凭该列表并不足以作为证据,安装程序的摘要也不够。 2026-08-27 实测:为 Claude Code 和任意通用 Agent 一起安装时,计划会显示 symlink → Claude Code,却完全不会创建 .claude/skills——最终摘要悄悄省略了这一行。Claude Code 读取的是 .claude/skills,而不是 .agents/skills,所以在尚无该符号链接的机器上,Claude Code 什么也得不到,但所有报告看起来都正常。检查链接本身(ls -l ~/.claude/skills);如果不存在,就将它指向集散目录(ln -s ~/.agents/skills ~/.claude/skills)。]

  8. 如果更改需要在当前会话中生效,就重新加载。 重新安装会修复磁盘上的文件;运行中的会话在启动时加载的副本仍是旧版,因此,在编辑技能后继续使用它的会话,用的仍然是被替换掉的内容。在 Claude Code 中,运行 /reload-skills,然后再次调用该技能,以加载新版内容。已于 2026-09-01 确认:某个会话重写了 session-harvest 的一个步骤,推送、重新安装,并验证已安装副本与检出内容一致,但在用户执行重新加载前,会话仍使用过时的措辞。应说明推送、重新安装和重新加载中还有哪一步未完成,而不是只报告“已重新安装”,仿佛整个流程已经结束。此内容于 2026-09-13 从 session-harvest 移至此处;该技能现在会报告安装状态,不再包含这套流程。

重命名或删除技能还需要执行第二步。 安装是累加式的:CLI 会添加并更新源发布的内容,但不会删除源中不再发布的内容,因此,重命名后的技能会以旧名称无限期保留,同时又出现一个新名称的技能。2026-08-28 实测,重命名一个技能后,重新安装整个仓库报告有十一个技能、十个源;旧技能仍可加载,旧描述仍会争夺触发机会。这不只是杂乱的问题:重复技能正是下文指出应当设法避免的触发冲突。重命名或删除后,运行 skills remove -g --skill <old-name> -y 并重新检查数量。

迭代时无需每次编辑都推送。 CLI 将本地路径视为一等来源,因此,skills add ../my-skills --skill <name> 会按工作树当前内容进行安装。编写草稿时可使用此方式;但要将更改视为完成,必须先推送,否则下一次从远程安装时会悄悄将其还原。

无论哪种流程,安装都是双向复制——不存在 Agent 从检出目录实时重新加载的模式。无论采用哪种流程,编辑后的内容只有在安装之后才会生效。

发布技能仓库

将技能放在公共仓库的 skills/<name>/SKILL.md 中。无需清单、无需市场条目、无需供应商目录——CLI 会在标准位置中最多向下搜索三层来发现技能,之后任何人都可以安装:

npx skills add <owner>/<repo> --global

用于记录仓库自身接口的技能,应提交到该仓库的 .agents/skills/ 下,这样在该仓库中工作的任何人都无需安装。跨项目的约定类技能应放在专用的技能仓库中。两者使用同一条命令安装。

名称是主键,却没有任何提示

名称可以随你喜欢来选,但采用之前先检查它是否可用,因为名称首先是标识符,其次才是标签,而所有使用它的机制都会静默失败。

python3 <path>/names.py check <candidate>     # before adopting it
python3 <path>/names.py audit --mine <you>    # every skill you already have
python3 <path>/names.py audit --offline       # local duplicates only, no network

退出码 0 表示无冲突,1 表示存在冲突,3 表示无法确定——无法访问的注册表会报告为未知,而绝不会报告为无冲突,因为无法运行时仍静默通过的检查比不检查更糟。

该脚本防范的问题,每一项都已在安装程序自身的源码中得到验证:

  • 重复名称会被悄无声息地丢弃。 vercel-labs/skills 会跳过已经见过名称的技能——按遍历顺序先到者胜。因此,安装其他人的仓库可能会让你的某个技能从列表中消失,而且没有任何地方会提示。
  • 锁文件以名称为键,因此重命名不会被识别为移动,而会被视为删除;新名称则会被当作从未安装过的技能。
  • 名称在比较前会被规范化——不区分大小写,并将空格和下划线转换为连字符——因此,My Skill、my_skill 和 my-skill 是同一个标识,而不是三个。
  • 技能没有变通办法,插件却有。 Anthropic 为插件提供了 displayName 和实时的 renames 映射,正是因为别名变更会破坏安装;技能格式两者都没有,因此名称同时是标识符和标签。

截至 2026-09-12,对照公开索引测得:这个语料库的 15 个名称中,有 5 个已由其他仓库发布——其中 skill-authoring 被 6 个仓库使用,包括两个供应商仓库。从创作者一侧看不到这些情况,而且这 5 个名称都早于这项检查。

[PITFALL:本地冲突总是发生在_不同作用域之间_,而源代码检出目录不属于作用域。 两个冲突的技能无法共存于同一个 hub——后者会覆盖前者——因此真正的冲突发生在用户 hub 与项目自身的 .agents/skills 之间;规范规定项目优先,并预期会发出警告。将源代码 skills/ 与 hub 一起审计,则会报告每个源代码仅领先于其安装版本的技能,而这正是日常创作中的常态。编写本节时已确认:编辑此文件就让检查将 skill-authoring 标记为与其自身的已安装副本冲突。因此,默认根目录是加载器的作用域,--root skills 是有意进行的创作检查,而摘要值用于判断两个副本是同一个文件还是两个不同文件。]

重命名已发布的技能属于破坏性变更。 这会让所有现有安装失效,而旧别名会永远留在公开索引中:此仓库曾重命名一个技能,重命名前的别名至今仍列在索引中,旁边还有它自己的安装次数和当前名称。应将重命名视为弃用,而不是编辑;最好趁还没人使用时就把名称定好。

没有可遵循的风格规范,而这本身就是一项发现,并非缺失。 规范的创作页面没有相关内容;没有已发布的评测会改变技能名称;文档说明的选择流程只依据描述。唯一存在的社区规则——优先使用动词和动名词——与语料库的实际情况大约以六比一的比例相反,语料库主要采用名词。因此,应把精力花在实际用于选择的描述上,而名称只需花这项检查所需的两秒。

references/naming.md 收录了语料库(1,043 个技能,40 个仓库)、规范的要求及其约束力有多有限、预算受限时唯一一个完全不显示描述而只渲染名称的测试框架,以及相关生态系统中可迁移和不可迁移的命名规则。

按职责拆分技能,并让触发条件互不冲突

编写新技能之前,先明确两件事——同一个决策从编写端和检索端来看:

  • 职责明确且单一。 不是主题,也不是一组碰巧需要一起使用的内容。涵盖两项职责的技能必须在一个 description 中同时描述它们,而字段匹配实际依赖的正是这个字段,因此每个提示词都要承受描述被稀释的代价。
  • 触发条件不与任何其他已安装技能的触发条件冲突。 避免冲突是硬性要求,不是锦上添花。若某个描述抢先匹配了原本面向同类技能的提示词,那么即使它自身适用的情况都通过了测试,也仍然是缺陷——这种失败难以察觉,因为错误技能加载后看起来和正确技能加载一模一样。

当新技能的触发条件与现有技能重叠时,解决办法是重新划分两者的边界,而不是更谨慎地措辞描述。举例来说:在 2026-08-28 之前,本技能和 mcp-server-shipping 曾合并为一个技能,描述为“开发或分发 MCP 服务或技能仓库”——一个描述涵盖两项职责,而且任何有关编辑技能的提示词都必须胜过一段有一半内容在讲 MCP 服务的描述。按职责拆分后,两段描述都更短、更精准。

从请求端来写 description:写人们实际会输入的词,而不是该主题自己的术语。用内部行话构建描述,造成的是结构性触发不足,不是运气不好——有个经过实际测量的例子:某个测试约定技能的描述写的是 test structure (DAMP vs DRY, fixture scope),这是技能描述自己的用词,因此在遇到“编写测试”“pytest”“fixtures”“parametrize”时都未能触发。

从请求端出发,指的是描述_问题_的用词,而不是解决问题的工具——这是同一条规则更精确的说法,也是谨慎的作者仍会犯错的地方。2026-08-31 的测量结果:某个技能的描述提到了 tasks.py、inv 和“namespace”,于是每个使用这些词的请求都会触发它;但面对“我们的自动化脚本越来越杂乱、不一致,该从哪里开始整理?”时,三次运行都完全没有选中任何技能——这才是人们实际感受到问题时的表达。它不是输给了竞争技能,而是输给了沉默,这种情况任何冲突检查都无法发现。

不要只靠阅读来决定这些事。用 skill-fitness 进行测量,本节只说明原则,具体机制由它负责:

python3 <skill-fitness>/scripts/fitness.py overlap        # ranked pairs, and directional shadowing
python3 <skill-fitness>/scripts/trigger.py run <cases>    # which skill a real request actually selects
python3 <skill-fitness>/scripts/trigger.py candidate ...  # score a proposed description before adopting it
python3 <skill-fitness>/scripts/trigger.py split ...      # how requests distribute across a proposed split

该工具的两项结果会影响本节的应用方式。它的静态重叠标记只是生成假设的工具,不是定论——它唯一可检验的预测被实际运行推翻了,因此应通过 trigger.py 来证明重新划分边界是合理的,而不是依据重叠评分。候选描述的得分则是一个可能高估也可能低估的估算值:提案会被登记为命令文件,而真实技能不会,因此即便措辞有所改善但尚未达到完全通过,仍值得采用——但发布后运行 run 得到的结果才是实际依据,不是走过场。 这里曾将其称为下限,直到 2026-09-01:同一组案例中,某个技能作为候选项时测得 12/12,安装后却是 11/12。

推论:当某项发现需要归属时,优先扩展已经负责该主题的技能,而不是新增一个技能。技能数量本身就是上下文负担,而且每多一个描述,选择时就多一处可能引发混淆的地方。

何时完全不该使用技能

技能会在触发条件满足时有条件地加载。如果某种行为必须无条件地应用于每个会话、每种 Agent 工具,那它就应该写进 AGENTS.md,而不是技能——每个执行环境都会读取始终加载的指令文件,不会漏掉触发条件。

有一次值得记住的反复实践证实了这一点:先安装了一个简洁沟通风格技能,之后又把它复制到 AGENTS.md,以便在所有工具中始终生效。两者同时存在后,技能就多余了,而它唯一剩下的区别特性(可切换的强度级别)带来的复杂度,超过了这种行为本身的需求。于是卸载了技能,只保留 AGENTS.md 中的副本,并精简为一种始终生效的模式。

反过来的判断更有用:遗漏后果悄无声息且代价高昂的规则,应该写入始终加载的文件;触发条件明确、遗漏后果轻微且可补救的规则,则适合写成技能。

正文和 references/ 中分别放什么

正文是 Agent 必须遵循的内容。其他一切——先例、测量结果、被否决的方案、规则背后的故事——都放进 references/,仅在需要时加载。

  • 如果规则源于实际发生的事情,请标注日期并引用证据(“2026-08-23 实际验证:……”)。有故事支撑的规则经得起审查;而没有依据的断言,会被下一位持不同意见的编辑弱化。
  • 如果发现规则在实践中经常被忽略,应强化措辞,而不是加长解释。 更长的理由不会提高遵循率;更明确有力的指令才会。
  • 不要纳入特定机器上的事实,或明确说明这些事实。依赖某台机器配置的规则——特定的 dotfile、本地安装的任务运行器、只存在于一台机器上的仓库——要么清楚说明这种依赖,要么就不该放进公开发布的技能中。
  • 发布规则前先试用,并期待试用能让规则变得_更明确_,而不只是通过或失败。 先在一个真实仓库中尝试某种惯例,熟悉的理由是找出那些徒增干扰或容易出错的规则。更有价值的结果,是让规则变得可以明确判断。2026-09-01 实际验证:起初的立场是,NamedTuple“成本很低,却能大幅提高可读性”;将它应用于一个 3,200 行的模块后,得出的规则却不是简单的是或否——把普通元组改成 NamedTuple 是升级,把冻结数据类改成 NamedTuple 则是降级,因为收益和风险其实是同一属性的两面。无论读多少资料,都不可能得出这个结论,而最终发布的版本是一项作者可以实际应用的测试,而非个人偏好。因此,如果试用只证实了你原本的看法,就值得对这次试用保持警惕。

技能可以确定性推导的内容,应写进脚本,而不是散文

如果一个合格的脚本能生成答案,技能就应附带该脚本,并在正文中说明要运行哪个标志。 SKILL.md 是模型每次都会从头重读的说明,因此,一段解释命令如何拼写的文字,就成了每次运行时都必须正确遵循的规则;而脚本只需遵循一次,之后只需调用。出错的代价也并非均匀分布:散文可能悄无声息地出错,而且看起来似乎合理——分支名称错了、漏了一个标志、过滤器吞掉了退出代码——输出看起来仍像个答案。

对于语法明确的接口,这一点最为突出:CLI 的标志和子命令、HTTP API 的请求和响应结构、SQL 查询、JSON 遍历、解析某个工具的输出。 这些都是确定性的,也并非易事,而且模型无法从重新推导它们中获益。任何多步骤操作也一样,只是更高一层:“运行这个,读取那个,然后根据结果运行另一个东西”本质上就是程序;把它写成散文,就是让 Agent 充当解释器。

实测案例,2026-09-02:session-harvest 有 967 行散文,其中机械性内容的一半每次运行都要重新推导——这台机器上 1,134 份转录中共有 24,429 次 Bash 调用,包括 568 次 plans-store 状态查询、498 次手写的 ahead-counts,以及 164 段针对转录编写的专用 Python heredoc,没有两段相同。它的六条规则都是因为某个具体错误反复出现才写下的;每条都已经用散文反复强调过,却还是不断重犯。把它们变成代码后,错误不再反复出现:不是因为措辞改进了,而是因为不再需要遵循任何措辞。

合理保留在散文中的内容,这样就不会变成“SKILL.md 里不能写命令”:

  • 读者只需运行一次的固定字面命令,不需要根据上下文拼接任何部分 (npx skills add <owner>/<repo> --global);
  • 对技能自身脚本的一次调用——包括占位符在内,这本身就是委派;
  • 每年运行不到两次的一次性紧急操作(如清除历史记录),此时脚本被阅读的次数会比散文更少;
  • 用于指定调用方仓库自身工具的命令,可移植脚本不得将其硬编码——技能无法知道某个仓库的门禁是 inv quality.precommit 还是其他命令,因此只需写“运行仓库的门禁”即可,不必多说。

应进行审计,而不是盲目信任,因为这正是最容易发生偏移的地方。 技能每次只改进一句话,而每条新增的命令行看起来都无伤大雅。 python3 <skill-fitness>/scripts/fitness.py derivable 会将语料库中的每条围栏命令划分为委派、可推导和固定三类,为每条命令标记其推导类型,并与已保存的基线进行差异比较——某项技能的可推导命令数量增加,就是审计结果。技能状态良好时保存基线;经过一轮修改后再进行比较。

scripts/ 中的脚本要声明自己的依赖,或者不依赖任何内容

默认只使用标准库,这并不是需要道歉的限制。 技能通过复制文件安装;之后不会运行安装步骤,因此,运行前需要解析任何依赖的脚本,对安装该技能的人来说就是无法使用的脚本。

  1. 仅使用标准库。 python3 <skill>/scripts/foo.py 在任何装有 Python 的机器上都能运行,无需声明,也无需解析依赖。本仓库中有两个真正的工具就是这样构建的——plan-docs/scripts/plans.py 和 session-bash-audit/scripts/audit.py。后者会解析每个 ~/.claude/projects/*.jsonl——也就是 Claude Code 自己的转录记录存储,因此它在其他运行框架上不会读取任何内容——并且仅使用 argparse/json/re/dataclasses/pathlib。简单的 YAML frontmatter 不足以成为使用 PyYAML 的理由。
  2. 如果确实需要依赖,则使用 PEP 723 + uv run:在文件中内联一个 # /// script TOML 块,从而保持单文件且可移植。这是技能脚本有文档记录的惯例,不是本地发明。
  3. 在技能中使用虚拟环境、requirements.txt 或 pyproject.toml——不行。 这会导致技能无法运行,除非有人执行安装步骤,而 skills CLI 并不会执行该步骤。

引用脚本时,使用相对于技能目录根目录的路径,这样所有安装该技能的人都能以相同方式使用它。

无论脚本使用什么语言编写,这里的脚本都是由 Agent 而非终端中的人运行的:不要使用交互式提示(Agent 的 shell 是非交互式的,TTY 提示会永远挂起);提供真正可用的 --help,因为 Agent 会通过它了解接口;将结构化输出写入 stdout,将诊断信息写入 stderr;记录退出码;并限制输出,因为运行框架会截断输出。

技能的输出是给运行它的人看的

在技能打印一条发现结果之前,先问问谁能据此采取行动。 如果答案是其他人——技能作者、另一个仓库的会话、运行者不拥有的机器——那么技能要么将发现结果发送到可以据此采取行动的地方,要么默认不打印。报告读者无法更改的内容并不算信息;它是读者每次运行都要付出的代价,而且会让读者学会不再看输出。

这通常有三种处理方式,按优先检查顺序排列:

发现结果应由谁处理技能的处理方式
运行技能的人打印
另一个仓库或会话转发——将其记录到可以据此采取行动的地方
技能本身的作者选择启用——放在命名的子命令之后,绝不设为默认行为

[PITFALL:你自己的机器无法显示这个 bug,所以需要特意检查,而不能等着它自己显现。 在作者的机器上,安装枢纽和源代码检出中的名称相同,因此,关于“已安装技能”的报告也会涉及作者可以编辑的文件,看起来似乎正确。但只要两者不同,在其他所有机器上都是错的——也就是说,除了作者自己的机器,其他机器全都如此。已于 2026-09-02 确认:按照陌生用户的输入方式运行审计,返回了 34 条发现结果,没有一条是读者能够处理的,而且没有指出他们真正使用的两种路径。请先在伪造的 HOME 下复现,再相信报告对读者是安全的。]

说明测量结果描述的是哪个集合。 计数是对特定文件集合的描述,而工具通常可以针对多个彼此不一致的集合运行——工作树、已发布内容、已安装内容。2026-09-03 在一台机器上测量时,工作树干净,也没有发生任何异常,但这三个集合仍然各不相同。请在标题中注明统计范围,并记住:已提交但尚未推送的修复,对任何人都不起作用。

技能可以将内容放在哪里,以及可以对机器做出哪些假设

没有任何脚本会硬编码本地路径,更不用说作者开发环境中的路径。 即使作为设有保护条件的最后兜底方案也不行:这类路径最容易通过审查,因为它们只对一台机器有用,在其他任何地方都不可见——直到某人的目录碰巧与之匹配。要求用户指定(--checkout <path>,并在错误信息中指出这个标志)才是坦诚的失败方式。

六种位置,各有一种用途,按技能自身的 name 命名:

位置用途Windows 默认值
$HOME/<name>用户自己的材料:可浏览,通常是一个 git 仓库%USERPROFILE%\<name>
$XDG_CONFIG_HOME → ~/.config/<skill>/供用户编辑的配置%APPDATA%\<skill>\
$XDG_STATE_HOME → ~/.local/state/<skill>/历史记录、统计数据、基准数据、上次运行记录%LOCALAPPDATA%\<skill>\
$XDG_DATA_HOME → ~/.local/share/<skill>/工具需要且无法重新生成的数据%LOCALAPPDATA%\<skill>\
$XDG_CACHE_HOME → ~/.cache/<skill>/可重新生成、随时可以安全删除的内容%LOCALAPPDATA%\<skill>\Cache\
tempfile.mkdtemp()临时工作区——绝不能使用固定的 /tmp/<name>同一调用——绝不能使用字面路径

变量在任何平台上都优先采用;只有默认值因平台而异。 在 Windows 上设置了 $XDG_STATE_HOME 的用户,显然就是这个意思,因此应先查找环境变量;只有在未设置时,os.name == "nt" 分支才决定如何处理。漫游属性是区分两个 Windows 基础目录的关键:配置是用户希望带到另一台机器上的内容,而状态和缓存则是_这台_机器上的记录。每个脚本都重复写三行——platformdirs 才是合适的库,但不能使用,因为这些脚本是在未安装任何内容的裸 python3 环境下运行的,而 --global 技能安装完全不会执行安装步骤。

判断依据是用户的材料还是工具的记账数据。人们会打开、搜索并进行版本控制的材料,应放在醒目的 $HOME 目录中——XDG 基础目录是供_应用程序_管理的内容使用的,这也正是伴随而来的用户目录规范存在的原因;没人会把 ~/Documents 放进 ~/.local/share。具体判断方法:人会不会在这里执行 cd? 如果会,它就不是记账数据;git 工作树绝不能放进 XDG 基础目录。

由此得出四条规则,每条都简单易行,且都是吃过亏才学会的:

  • 始终按同一顺序解析:显式参数 → 技能自身的环境变量 → $XDG_* → 默认值。XDG 目标路径无需新增变量——它继承用户已经控制的变量,因此使用它能减少设置,而非增加设置。
  • **绝不要写入自身的安装目录。**重新安装时,这个目录中的内容会被替换。 已于 2026-09-03 确认:有个技能让读者把基线保存到 ~/.agents/skills/<name>/references/baselines/,于是它要求任何人保留的唯一一份真正属于单台机器的状态,就被放在了专门设计为会被覆盖的位置。
  • 始终将其他工具的目录视为只读——如果该目录不存在,应报告不可用,而不是报告零;否则,一台从未运行过该工具的机器就会被误认为一切都未发生过。
  • **内容私密时,在创建时设置 0700,之后不要再检查。**传入该模式无需额外成本:umask 只能进一步收紧权限,Windows 会忽略它,因此无需分支,也无需检测功能是否可用。应在根目录上设置,因为 parents=True 不会将其应用于中间目录,而 0700 根目录会阻止访问其下的所有内容。检查并非没有成本——在 Windows 上,st_mode 是根据文件属性合成的,因此目录会被显示为所有用户都可访问,继而触发警告,而这里讨论的权限概念在 Windows 上并不存在,读者也无法执行建议的补救措施。值得保留的是开箱即用的默认行为;没人能采取行动的警告,正是本文花最多笔墨反对的事情。

技能之间不能相互依赖。它们是单独安装的,采用复制而非符号链接的方式安装,而且全局安装不会写入锁文件——因此,导入同级技能就意味着硬编码本节禁止使用的中心目录路径、跨越一个没有版本管理的边界,而且行为会因读者恰好安装了哪些其他技能而改变。应将位置作为配置共享,供双方读取(例如环境变量、配置文件);宁可重复十行解析器代码,也不要依赖任何东西;如果某个技能确实应调用另一个技能,就让 Agent 来执行,并明确说明因同级技能未安装而跳过了调用。

说明技能会读取、运行和写入什么——并仔细审查每一项写入

如果技能附带脚本,或要求读者运行会在会话仓库之外写入内容的命令,就必须包含一个标题为 ## What this skill reads, runs and writes 的章节,并列出四项:Reads、Runs、Writes、Network——即使最后两项的答案是“没有”,也要列出,因为对决定是否信任该技能的读者来说,“不写入任何内容”是文件中最有用的一行。应按类型说明它会修改什么(会话仓库的 plans/、某个存储区、它拥有的配置),而不是列出会随时间变动的路径;同时说明哪些子命令会写入,哪些不会。环境相关内容——产品、它调用的工具、网络访问——应放在 compatibility 前置元数据字段中,这是读者首先会查看的地方。纯粹约定类技能则两者都不需要。

这是披露声明,不是权限清单,这个词很重要。 没有任何机制会检查声明是否属实,也不可能做到;技能对自身的陈述并非控制措施。MCP 自身的注解也注明,客户端必须将其视为不可信信息,这一点在这里同样适用。披露声明的价值在于可以手动对照代码核实——持怀疑态度的读者只需用一次 grep 就能确认“没有网络客户端”;布局测试也可以检查声明是否存在,以及是否包含 Writes 行,而这就是测试能够诚实检查的全部内容。

应根据每项写入的恢复方式进行审查,而不是看它是否获准。 没有人在授予权限,所以“是否允许”问错了问题;真正能区分“即使没有恶意也很危险”和日常操作的是可恢复性。Git 仓库内的写入有差异对比、还原操作和历史记录。写入每个仓库之外、未纳入版本控制的文件则三者皆无,这种写入才需要在披露声明中说明理由,或者直接去掉。按此标准,有四种常规写入,每一种都应明确声明,而不是默认不提:

  1. 通过自身命令修改自身配置(plan-docs 的 config set 命令修改 ~/.config/plan-docs/config.toml)——绝不能让技能要求读者手动编辑。
  2. 写入其声明用途所指定的目标位置(research-library 克隆到 $RESEARCH_HOME,脚手架工具生成新仓库)——应说明判断标准,而不是列出实例,因为第二种情况是后来调查时才发现的。
  3. 通过 plan-docs 写入两级 plans 存储区——遵循该技能自身的条件:如果存储区有未提交更改,就新建文件,而不是修改另一个会话可能正在使用的文件。
  4. 写入会话自身的仓库。 其他所有位置——另一个仓库的目录树、已部署的指令文件、已安装的技能副本——都不在范围内;session-harvest 在其流程开头一次性说明了这一范围,可作为范例。

此规则针对文件写入和提交。非文件副作用——例如终止孤立进程、清理缓存——由提出这些操作的技能负责管理,而且这些操作只会被提议,不会实际执行。

发布前及一轮编辑后的语料审计

从 2026-09-02 到 2026-09-05,这套语料通过一轮循环,将 34 条读者无法采取行动的发现降至零。这套方法具有普适性。发布前在工作树上运行一次,每轮编辑 SKILL.md 后再运行一次;每一步都是 skill-fitness 命令,因此无需在此重新推导:

  1. portability — 每条未说明的引用都属于四种情况之一,应针对每条发现选择修复方式,绝不能批量改写:声明它(“在此作者的机器上”,说明某个位置由哪个运行环境管理,或引用某台机器曾有的带日期测量结果);泛化它(仓库自己的门禁、<owner>/<repo>,或使用检测到的路径,而不是指定路径);将它作为示例(“示例中列出的是作者的服务器——请替换成你自己的”);或删除它。对于脚本命中项,删除是唯一的修复方式,因为代码无法声明。描述中不能包含声明,因此要从正文继承声明——请在正文中写明。
  2. **先以读者身份复现,再相信结果。**你自己的机器无法显示面向读者的错误,因为在那里,中心目录和检出内容中的名称相同。测试方法是使用一个安装了你的两个 skill 的虚假 HOME;本仓库的 tests/unit/test_audience.py 展示了如何将它设为永久门禁。
  3. derivable --compare <baseline> — skill 的可推导项数量增加,就是一条发现;请阅读样例,因为合理的残余项(外部 CLI 自带的单行命令、使用方仓库自己的门禁)会与偏差一起输出。
  4. **在声称读者看到的问题已修复之前,先运行 --ref origin/main。**已提交但未推送的修复,对任何人都不算修复;标题行会说明数字所描述的群体。
  5. 归档推动这些编辑的计划,并在其中用 ## Migrated to 指明上文各节,这样下一次审计就能从规则开始,而不是从故事开始。

仍需判断、且不纳入循环的事项包括:某条发现适合用上述哪种修复方式;是否可以精简描述中的触发词汇(请先用 trigger.py 测量);以及披露内容是否属实。

约定类 skill 应在遇到阻碍时自我更新

编码约定(而非执行一次性任务)的 skill,应从实际使用中不断改进,而不只是供人阅读和遵循。默认做法是:使用它时,如果遇到其规则无法解决的真正歧义,或用户纠正了它做出的决定,就先询问而不是猜测;然后将解决方案作为一项小幅增补写回源文件,并按照上述流程重新部署。session-harvest 中的 “On friction, ask” 和 “Self-update mechanics” 部分就是可借鉴的示例。

skill 的后续检查应由它执行,而不是转交给用户

如果 skill 自己的研究最终要求“过一周后重新测量”“在线验证 X”“与基线比较”,这些都是 skill 的工作,而不是用户的:应将每项要求编码为 skill 在下次调用时执行的操作——带通过/失败输出的脚本标志(如 --compare <baseline>,并给出逐项预期结果)、由 skill 保存并进行差异比较的基线,或包含预期结果的探测计划,由 Agent 逐步执行。真正无法自动化的事项(例如由人工留意权限提示)应简化为一个是/否问题,而不是留作编号待办。2026-08-24,用户针对一个以手动“打开/重新测量”清单收尾的 skill 表示:“我不想手动做这些,skill 应该替我做。”session-bash-audit 的 Measure / Compare / Probe 划分就是这种模式。

完整理由

references/rationale.md — 说明自我更新为何具有普适性,而非某一个 skill 的特例;以及为何描述技能发布方式的 skill 必须讲述可移植的机制,而不是作者自己的自动化方案。

评论

支持 Markdown