动手写一个 Skill
Skill 就是一个 SKILL.md 文件:YAML 前置元数据 + Markdown 指令内容。放到对应目录,Claude Code 自动发现,需要时加载。
Claude Code 已经把「自定义命令」(
.claude/commands/)合并进了 skill 体系。.claude/commands/foo.md和.claude/skills/foo/SKILL.md都生成/foo,写法相同。Skill 的额外能力:附加文件目录、frontmatter 控制谁能触发、Claude 可以根据 description 自动调用。
写之前:想清楚四个问题
- 这个 skill 让用户能做什么? — 之前做不到、现在能做到的事
- 什么时候触发? — 用户会说哪些话、输入什么命令
- 期望的输出格式是什么? — 纯文本回复?代码 diff?生成文件?
- 能不能客观验证输出? — 能验证(比如「必须生成
.json文件」)就值得写测试;输出主观(写作风格、设计品味),测试意义不大
文件结构
my-skill/
├── SKILL.md # 主指令(必需)
├── scripts/ # 辅助脚本(可选,被执行而非读入上下文)
├── references/ # 参考资料(可选,按需加载)
└── assets/ # 模板等资源(可选)目录名就是 skill 名,输入 /目录名 即可触发。
Skill 内容的两种类型
Reference 型:给 Claude 加知识——约定、模式、风格指南、领域常识。会与当前会话一起被引用。
---
name: api-conventions
description: API 设计约定
---
写 API 端点时:
- 用 RESTful 命名
- 错误格式统一
- 加请求校验Task 型:一组明确的步骤——部署、提交、生成代码。通常你想自己 /skill-name 触发,加 disable-model-invocation: true 防止 Claude 自作主张调用。
---
name: deploy
description: 部署到生产
context: fork
disable-model-invocation: true
---
部署应用:
1. 跑测试
2. 构建
3. 推到部署目标正文要简短:skill 一旦被调用,内容会一直留在上下文里跨多轮,每行都是持续的 token 成本。能写「做 X」就别写「你应该做 X 因为……」。
Frontmatter 字段
| 字段 | 说明 |
|---|---|
name | skill 名称。小写字母 / 数字 / 连字符,≤ 64 字符 |
description | 最关键——告诉 Claude 这个 skill 做什么、何时触发。description + when_to_use 合计被截到 1536 字符,把核心场景写前面 |
when_to_use | 补充触发场景或例句,追加在 description 后 |
argument-hint | 自动补全时显示的参数提示,比如 [issue-number] |
arguments | 命名参数列表,内容里用 $参数名 引用 |
disable-model-invocation | true 则只能手动 / 触发,Claude 不会自动调 |
user-invocable | false 则从 / 菜单隐藏,只能 Claude 自动调 |
allowed-tools | skill 激活期间免确认的工具,如 Bash(git *) |
model | skill 激活期间使用的模型,或 inherit |
effort | 强度档位:low / medium / high / xhigh / max |
context | fork 则在独立 subagent 中运行,不污染主会话上下文 |
agent | 配合 context: fork:Explore / Plan / general-purpose 或 .claude/agents/ 中的自定义 agent |
hooks | 此 skill 生命周期内生效的 hooks |
paths | glob 匹配,只在操作特定文件时自动加载 |
shell | bash(默认)或 powershell |
字符串替换
Skill 内容支持几种动态替换:
| 变量 | 含义 |
|---|---|
$ARGUMENTS | 调用 skill 时传入的全部参数 |
$ARGUMENTS[N] / $N | 第 N 个参数(0-based),$0 是第一个 |
$name | arguments 里命名的参数 |
${CLAUDE_SESSION_ID} | 当前会话 ID |
${CLAUDE_EFFORT} | 当前 effort 档位 |
${CLAUDE_SKILL_DIR} | 当前 skill 所在目录——bash 注入命令里引用打包脚本时用这个,跨安装位置都能解析对 |
带空格的参数用引号包起来,shell 风格 quoting。/my-skill "hello world" second 里 $0 = hello world,$1 = second。
最简示例
总结未提交改动的 skill:
1. 建目录
mkdir -p ~/.claude/skills/summarize-changes2. 写 SKILL.md
---
description: 总结 git 未提交的改动,标出风险点。用户问「改了什么」「帮我写 commit message」时自动触发。
---
## 当前改动
!`git diff HEAD`
## 指令
用两到三个 bullet 总结上面的改动,然后列出你注意到的风险:缺少错误处理、硬编码值、需要补测试的地方等等。如果 diff 为空,直接说没有未提交改动。3. 测试
在任意 git 项目里开 Claude Code,输入 /summarize-changes,或者直接问「我改了什么」,Claude 会自动匹配 description 并加载 skill。
注入动态上下文
!`命令` 语法在执行 skill 前先跑 shell 命令,输出直接替换占位符。Claude 看到的是命令结果,不是命令本身:
---
description: 查看 PR 详情并总结
context: fork
agent: Explore
allowed-tools: Bash(gh *)
---
## PR 信息
- 改动文件:!`gh pr diff --name-only`
- Diff:!`gh pr diff`
- 评论:!`gh pr view --comments`
## 指令
总结这个 PR 的改动内容和潜在风险。多行命令用 ```! 代码块:
## 环境
```!
node --version
npm --version
git status --short
```想让 skill 在响应里多想一会儿,正文里加 ultrathink。
参数传递
---
name: fix-issue
description: 修一个 GitHub issue
disable-model-invocation: true
---
修复 GitHub issue $ARGUMENTS:
1. 读 issue 描述
2. 理解需求
3. 实现修复
4. 写测试
5. 创建 commit输入 /fix-issue 123,$ARGUMENTS 被替换为 123。如果 skill 没写 $ARGUMENTS,Claude Code 会把输入以 ARGUMENTS: <值> 追加到末尾。
Skill 存放位置
| 位置 | 路径 | 作用范围 |
|---|---|---|
| 企业级 | 由 managed settings 配置 | 组织内所有用户 |
| 个人 | ~/.claude/skills/<name>/SKILL.md | 所有项目 |
| 项目 | .claude/skills/<name>/SKILL.md | 当前项目 |
| 插件 | <plugin>/skills/<name>/SKILL.md | 启用插件的项目 |
同名 skill 的优先级:企业 > 个人 > 项目。插件 skill 走 plugin-name:skill-name 命名空间,不会冲突。如果 .claude/commands/ 和 .claude/skills/ 出现同名,skill 优先。
实时变更:Claude Code 监听 skill 目录,会话期间增删改 skill 立刻生效,不用重启。但如果是新建一个之前根本不存在的 .claude/skills/ 目录,要重启才会被监听。
Monorepo 友好:项目 skill 从启动目录沿父级目录一路向上查找到仓库根;当 Claude 操作子目录里的文件时,也会按需加载该子目录下的 .claude/skills/。
控制谁能调用
默认你和 Claude 都能调。两个字段限制行为:
| Frontmatter | 你可调 | Claude 可调 | 上下文加载时机 |
|---|---|---|---|
| (默认) | ✓ | ✓ | description 常驻;触发时加载正文 |
disable-model-invocation: true | ✓ | ✗ | description 不进上下文;你触发时加载正文 |
user-invocable: false | ✗ | ✓ | description 常驻;触发时加载正文 |
/permissions 里也能精细控制:
Skill(commit) # 允许 commit
Skill(review-pr *) # 允许 review-pr 加任意参数
Skill(deploy *) # 禁用 deploy
Skill # 禁掉全部 skillSkill 内容的生命周期
Skill 被调用时,渲染后的 SKILL.md 作为一条消息进入对话,之后整个会话都留在上下文里——Claude Code 不会在后续轮次重新读文件。所以写「全局生效的指引」用 standing instructions 的语气,而不是一次性步骤。
自动 compaction 会按 token 预算保留最近调用过的 skill:每个 skill 保留前 5000 token,所有 skill 共享 25000 token 池,从最近调用的往回填。如果 compaction 后某个 skill 不再影响行为,通常是模型选择了别的工具——加强 description,或重新 / 触发一次。
包脚本
Skill 可以打包任意语言的脚本,比 prompt 多一档能力——生成可视化 HTML、运行检查、调用本地工具。脚本路径用 ${CLAUDE_SKILL_DIR} 引用,跨安装位置都能解析:
---
name: codebase-visualizer
description: 生成代码库可折叠树状可视化
allowed-tools: Bash(python3 *)
---
跑可视化脚本:
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/visualize.py .
## 写测试用例
如果 skill 输出可以客观验证,按以下格式写 `evals/evals.json`:
```json
[
{
"id": "basic-usage",
"prompt": "用户的真实 prompt…",
"files": { "src/app.ts": "相关文件内容" },
"assertions": [
{
"id": "generates-config",
"description": "生成了 .claude/settings.json 文件"
}
]
}
]先写 prompt,断言可以后面补。好的断言用描述性的 id,表达可以被客观验证。
评估与迭代
- 跑测试用例 — 用 subagent 分别跑 skill 版和 baseline(无 skill 或旧版本),对比差异
- 写断言 — 跑的同时补充断言,好的断言是「客观可验证」的
- 分析结果 — 看哪些断言没通过、哪些没区分度、哪些不稳定
- 改进 skill — 四个方向:
- 泛化:是不是过拟合到测试用例了?
- 精简:有没有不产生价值的冗余指令?
- 解释为什么:把死规则改成原因说明
- 提取重复:所有测试里 subagent 都写了同样的辅助代码,就打包进
scripts/
- 重复 — 改进 → 重跑 → 对比 → 直到满意
优化 description
skill 写完、内容稳定后,回头调优 description 触发准确率:
- 生成 20 个 prompt(8-10 个应触发,8-10 个不应触发——后者要写「接近但不该触发」的,不是明显无关)
- 人工确认这些 prompt 合理
- 脚本自动跑多轮,找 test score 最高的 description
- 把最优 description 写回 SKILL.md
用 skill-creator 自动化整个流程
上面这套「写草稿 → 跑测试 → 看结果 → 改进 → 优化 description」的流程,Anthropic 把它封装成了一个叫 skill-creator 的 skill——一个「专门用来写 skill 的 skill」。装好之后你跟它说「想做一个 X 的 skill」,它会引你走完整套流程。
它做的事比手搓多两层:
- 用 subagent 并行跑测试:每个测试 prompt 同时 spawn 两个 subagent,一个带 skill、一个不带(baseline),结果存到
<skill-name>-workspace/iteration-1/eval-<id>/ - 量化 + 肉眼双轨评估:grader subagent 对照断言打分生成
benchmark.json(pass_rate / token / 耗时),同时启动eval-viewer/generate_review.py在浏览器里逐 case 看输出、留评论。用户提交后写到feedback.json,下一轮迭代直接读
核心循环:
- Capture Intent — 追问四个问题:能做什么、何时触发、输出格式、是否需要客观验证
- 写草稿 — 生成第一版 SKILL.md,description 倾向「pushy」一点(当前 Claude 偏向 undertrigger)
- 写 2-3 个真实测试 prompt 存到
evals/evals.json,先不写断言 - 同一轮里并行 spawn 所有 run(with-skill + baseline),别先跑一组再跑另一组
- 跑 grader、跑
scripts.aggregate_benchmark聚合,启动 viewer 给用户看 - 读 feedback、改 skill、进入 iteration-2,重复直到用户满意或反馈都为空
特别提一个细节:改进 skill 时,宁可解释 why 也不要堆 ALL CAPS 的 MUST/NEVER——当代 LLM 理解力够强,硬规则反而限制泛化。如果发现所有测试用例里 subagent 都独立写了同一段辅助脚本,就把它打包进 scripts/。
单独跑 description 优化
skill 内容稳定后,跑 description 优化器:
python -m scripts.run_loop \
--eval-set <trigger-eval.json> \
--skill-path <path-to-skill> \
--model <当前会话用的 model id> \
--max-iterations 5 --verbose工作方式:自动生成 20 条 trigger 测试 prompt(8-10 条应触发 + 8-10 条「接近但不该触发」的 near-miss),拆 60% train / 40% held-out test,每条跑 3 次取触发率,迭代多轮、用 test 集得分选 best description(避免在 train 上过拟合)。结束后浏览器里弹 HTML 报告。
什么时候用 skill-creator
- skill 输出能客观验证(生成文件、转换数据、固定步骤)——评估才有意义
- 你愿意花至少 1-2 轮迭代认真做这个 skill,而不是临时凑一个 prompt 应付
写作风格类(写作风格、设计品味)skill 跳过定量评估,直接走肉眼 review 那一条线就行。
故障排查
Skill 该触发没触发
- description 里加用户自然会说的关键词
- 问一句「有哪些 skill 可用?」看是否列出
- 换个说法接近 description
- 直接
/skill-name触发
Skill 触发太频繁
- description 写更具体
- 加
disable-model-invocation: true只允许手动
description 被截短
skill 名一定会进上下文,但 description 有总字符预算(默认按上下文窗口的 1%)。skill 多的时候,调用频率低的会先被截短。/doctor 能看到当前预算情况。提高预算用 skillListingBudgetFraction(比如 0.02 = 2%)或 SLASH_COMMAND_TOOL_CHAR_BUDGET;不重要的 skill 在 skillOverrides 里设 "name-only" 让出预算。单条 description+when_to_use 上限 1536 字符,可用 maxSkillDescriptionChars 调。
打包发布
python -m scripts.package_skill <path/to/skill-folder>