这篇想弄清楚几件事:Skill 从哪几个来源加载、谁覆盖谁?frontmatter 有哪些字段、各控制什么?模型和用户分别通过哪两条路径触发 skill?想给项目注入领域知识时文件该放哪?

Skill 就是一个 Markdown 文件

Skill 是一个 Markdown 文件 + YAML frontmatter。系统 prompt 里只放 name + description 的清单(省 token);模型判断匹配时才用 skill 工具把全文拉进上下文——这就是「按需加载」。

例Example text
启动 / 触发时:load_skill_registry()
  bundled → user → extra dirs → project → plugin
  (后注册的同名片段覆盖先注册的)
            |
            v
系统 prompt 注入清单:name + description(不含正文)
            |
   ┌────────┴────────┐
   v                 v
模型路径           用户路径
skill 工具         /skill-name
返回全文           全文渲染成 prompt 提交

下面的 Skill Lab 不是逐步 trace,而是一张可调实验台:选定覆盖来源 → 拨两扇门禁 → 分别用模型路径或用户路径触发,直接看清单是否可见、正文是否进入上下文。

图Fig. 清单常驻,正文按需Skill 实验台 · 本地模拟,不读磁盘也不调用模型Skill bench · simulated locally, no disk or model

加载顺序Load orderbundled → user → extra → project → plugin同名时后者覆盖前者for the same name, the later one wins

一谁覆盖谁

点击最晚注册、实际生效的那一层。项目领域知识优先放 project。

二两扇门禁

frontmatter 把「广告位」和「谁能拉正文」拆开。description 只影响模型是否想点名。

三模型默认只看到这些

系统 prompt 注入 name + description,不含正文。

deploy-checklist发布前检查清单,用户说「准备发布」时使用约一行广告位,token 很少about one line, few tokens

四两条路径,两种回灌方式

进了上下文的内容What reached the context正文未进入上下文the body is not in context

尚未触发

默认上下文只有清单(若允许模型看见)。点上方的一条路径,看正文何时进入。

system prompt catalog
- deploy-checklist · 发布前检查清单,用户说「准备发布」时使用
生效来源Winning source
project
家目录Base directory
.openharness/skills/deploy-checklist
清单Catalog
可见Visible

源码地图

  • skills/types.py:,skill 的 frozen dataclass。
  • skills/_frontmatter.py:YAML frontmatter 解析与 fallback 链。
  • skills/bundled/__init__.py:内置 skill,从 content/*.md 平铺加载。
  • skills/loader.py:多来源加载、项目目录向上发现、路径安全校验。
  • skills/registry.py:,多键注册与去重。
  • tools/skill_tool.py:模型路径,skill 工具按名取全文。
  • prompts/context.py:把 skill 清单写进系统 prompt。
  • commands/registry.py:用户路径,/skill-name 斜杠命令与变量替换。

从哪加载,谁覆盖谁

严格按此顺序注册,同名 key 后者覆盖前者(dict 赋值语义):

  1. bundled:skills/bundled/content/*.md,平铺 xx.md。
  2. user:~/.openharness/skills,兼容 ~/.claude/skills、~/.agents/skills;布局为 dir/skill/SKILL.md。
  3. extra dirs:运行时传入(如 ohmo workspace),布局同上。
  4. project:.openharness/skills、.agents/skills、.claude/skills,布局同上。
  5. plugin:已启用插件携带的 skills,插件内部路径。
项目注入落点

想给仓库注入领域知识、发布清单或团队流程:放在项目根的 .openharness/skills/<name>/SKILL.md(或兼容的 .agents/skills / .claude/skills)。它会覆盖同名 user skill;若启用插件也提供同名 skill,插件注册更晚,会再覆盖 project。

frontmatter 的五个控制字段

例Example SKILL.md · frontmatteryaml
---
name: deploy-checklist
description: 发布前检查清单,用户说"准备发布"时使用
user-invocable: true
disable-model-invocation: false
model: claude-opus-5
argument-hint: "<env>"
---

正文:模型/用户触发后拿到的完整指令……
  • name:主键;缺省用目录名(bundled 用文件名)。
  • description:写进系统 prompt 的一行——模型决定是否调用的唯一依据。
  • :从斜杠命令隐藏,只留给模型。
  • disable-model-invocation: true:不进系统 prompt 清单;skill 工具也会拒绝,只能用户手动触发。
  • model:用户路径用 让这条 prompt 跑在指定模型上。
  • argument-hint:参数提示,纯展示用(可选)。

没有 frontmatter 也能加载:fallback 链是 ① YAML frontmatter → ② 第一个 # 标题 + 首个正文段落(description 截断 200 字符)→ ③ "Skill: {name}" 模板。但缺 description 的 skill 模型几乎不会主动调用——description 就是 skill 的广告位。

Registry:一个 skill,多个键

把同一个 skill 挂到多个键上:name、command_name、display_name、aliases。所以用户敲 /plan 和模型查 "plan" 命中的是同一份定义。 再按 (source, path) 去重,保证清单里每个文件只出现一次。

模型走工具,用户走斜杠命令

模型路径

  • 入口:系统 prompt 清单 → 工具。
  • 返回:原文 content 作为工具结果。
  • 变量替换:无。
  • 门禁: 时拒绝。
  • 模型指定:无(当前会话模型)。

用户路径

  • 入口:/skill-name args...。
  • 返回:渲染后作为新 prompt 提交给模型。
  • 变量替换:ARGUMENTS、CLAUDE_SKILL_DIR、CLAUDE_SESSION_ID 等占位符(写作 ${…} 形式)。
  • 门禁: 时拒绝。
  • 模型指定:(frontmatter model)。

用户路径的渲染逻辑():

例Example render-skill-command.py · simplifiedpython
prompt = f"Base directory for this skill: {base_dir}\n\n" + content
prompt = prompt.replace("${CLAUDE_SKILL_DIR}", base_dir)
prompt = prompt.replace("${ARGUMENTS}", args)
# 内容里没写 ${ARGUMENTS} 但用户传了参数 → 末尾自动追加 "Arguments: ..."
return CommandResult(submit_prompt=prompt, submit_model=skill.model)

为什么用户路径要 prepend base_dir?skill 正文常引用同目录的辅助文件(脚本、模板)。告诉模型「这个 skill 的家在哪」,它才能用 read_file 等工具去取——你在 Claude Code 里看到的 Base directory for this skill: ... 就是这么来的。

两条路径不要混

模型路径是「工具结果回灌」;用户路径是「把 skill 渲染成一条新的用户 prompt」。变量替换、模型切换和 base_dir 注入只稳定出现在用户路径。

每次都重新加载,改完就生效

SkillTool.execute 和斜杠命令 handler 每次调用都重新 ——没有缓存。代价是每次重新扫目录、解析 YAML;收益是改完 SKILL.md 立刻生效,不用重启会话。这和 Hooks 热重载(每条消息前重建 registry)是同一哲学:配置即改即生效,用微小的 IO 换迭代速度。

一个最小的 SKILL.md

例Example project skill · minimalmarkdown
---
name: my-check
description: 一句话说清做什么 + 什么时机该调用我
---

# my-check

## When to use
(触发条件,写给模型看的)

## Workflow
1. 步骤一
2. 步骤二

## Rules
- 硬约束

落地建议:

  1. 项目知识放 project 目录,不要只放在个人 ~/.openharness/skills。
  2. 把触发条件写进 description,否则清单再全,模型也难主动点名。
  3. 需要用户手搓启动、不希望模型抢跑时,设 disable-model-invocation: true。
  4. 需要模型内化流程、不希望污染 / 补全时,设 。
  5. 辅助脚本与模板和 SKILL.md 同目录,依赖用户路径注入的 base_dir,或在正文里写清相对路径。

Skill 和 Tools、Hooks 的分工

  • Tools:通用能力接口(读文件、跑命令)。Skill 不是新工具类型,而是可按需注入的流程说明;模型仍通过已有工具完成动作。
  • Permissions / Hooks:安全闸门与生命周期扩展。Skill 改变的是「模型接下来按什么剧本想问题」,不替代执行前鉴权。
  • Context / Compact:清单常驻系统 prompt,正文按需进入对话;过长 skill 正文同样会占用上下文预算,仍可能触发 compact。

一句话:Tools 给手,Skills 给剧本;Permissions 与 Hooks 决定剧本里的动作能不能真的落地。

精读清单

主读:src/openharness/skills/loader.py —— 来源顺序、项目发现与路径安全。

接着看:

  • src/openharness/skills/registry.py、types.py —— 多键注册与定义形状;
  • src/openharness/skills/_frontmatter.py —— 字段与 fallback;
  • src/openharness/tools/skill_tool.py —— 模型路径与 ;
  • src/openharness/commands/registry.py —— 用户斜杠路径与变量渲染;
  • src/openharness/prompts/context.py —— 清单如何进入系统 prompt。