本教程带你从零创建一个会被 Claude 精准触发的自定义 Skill。重点不在"写一个能跑的 markdown",而在触发工程(Trigger Engineering)——为什么你的 Skill 写了却不被调用、description 预算是怎么分配的、如何用 paths/when_to_use//doctor 让它在正确时刻加载。适合已跑通 Claude Code、想把重复指令固化成可复用技能的开发者。
paths、when_to_use、动态上下文注入等较新特性)mkdir、cat,看得懂 YAML 与 Markdown很多人把 Skill、CLAUDE.md、commands、MCP 混为一谈。这一步不搞清楚,后面写出来的 Skill 注定"不触发"。
| 机制 | 何时进入上下文 | 适合放什么 |
|---|---|---|
| CLAUDE.md | 每次会话都加载 | 事实型约束(项目约定、技术栈) |
| Skill 主体 | 只在被触发时加载,描述常驻 | 流程型步骤、可复用工作流 |
| commands(旧) | 与 Skill 等价,已被合并 | 已存在的 .claude/commands/*.md 仍可用 |
| MCP server | 进程常驻,提供工具 | 需要稳定 API/外部数据源 |
关键区别就一句:CLAUDE.md 的内容每轮都烧 token,Skill 的正文只有在被调用那一刻才进上下文。所以"一段越长越长的流程",应该从 CLAUDE.md 搬进 Skill,而不是继续往 CLAUDE.md 里塞。判断标准也很简单:当 CLAUDE.md 的某段已经从"事实"长成了"操作步骤",就是它该变成 Skill 的时候。
Skill 遵循开放的 Agent Skills 标准,可跨多个 AI 工具复用;Claude Code 在此之上扩展了调用控制、子代理执行、动态上下文注入等能力。
跳过这一步直接动手,是 Skill 不触发的头号原因。先用三句话钉死你的 Skill:
from-tag、to-tag → 分类好的更新日志。本教程的实战案例就定为 release-notes:读取两个 git tag 之间的提交历史,按 Conventional Commits 规范归类成"新功能 / 修复 / 破坏性变更 / 其他",产出一份发布说明。它和"提交信息生成"不同——后者针对单次 commit,前者针对整段发版区间。
个人级 Skill 放在 ~/.claude/skills/ 下,跨你所有项目可用;项目级放在仓库的 .claude/skills/,只随这个仓库生效。先建目录:
mkdir -p ~/.claude/skills/release-notes/scripts
mkdir -p ~/.claude/skills/release-notes/examples
标准结构长这样,只有 SKILL.md 是必须的,其余按需:
release-notes/
├── SKILL.md # 入口,必填:frontmatter + 指令
├── template.md # 输出模板,Claude 按它填
├── reference.md # Conventional Commits 分类规则,按需加载
├── examples/
│ └── sample.md # 一份期望输出样例
└── scripts/
└── categorize.sh # 可执行脚本,被运行而非被读
目录名 release-notes 会自动成为命令 /release-notes,和 frontmatter 里的 name 字段无关(name 只影响列表里的显示名)。这是新手最常踩的坑之一:改了 name 却纳闷为什么 /xxx 还是旧名字。
整个 Skill 最值钱的一行就是 description。Claude 在每轮对话里,拿着用户的话去和所有 Skill 的描述做匹配,决定要不要把某个 Skill 的正文加载进来。描述写得泛或写得偏,触发就走样。
先看一份反例:
---
description: 帮助生成文档
---
问题在于"文档"太宽,Claude 在你让它写 README、写注释、写 API 文档时都可能误触发,也可能都不触发。再看正例:
---
name: release-notes
description: 为当前或指定版本生成发布说明(release notes / changelog)。当用户询问"这次发版改了什么""上一次 tag 到现在有哪些变更""帮我写 v2.3.0 的更新日志"时使用。
when_to_use: |
触发短语举例:
- "帮我写这次发布的 release notes"
- "上个版本到现在改了什么"
- "为 v2.3.0 生成更新日志"
- "整理这周的 commit 做发布"
argument-hint: "[from-tag] [to-tag]"
arguments: [from, to]
allowed-tools: Bash(git log *) Bash(git tag *)
paths: "src/**, package.json, CHANGELOG.md"
---
这里每一行都在为触发服务,逐个拆解:
description(推荐):第一句话就把"做什么 + 什么时候用"说死,关键词用用户原话(发布说明、changelog、更新日志)。官方规则是 description + when_to_use 合起来在技能列表里最多 1536 字符就被截断,所以要把最关键的用例放在最前面。
when_to_use(可选):专门塞触发短语和示例请求,会被拼到 description 后面。这是让 Claude"听懂人话"的利器——与其描述功能,不如直接列出"用户会怎么说"。注意它也算在那 1536 字符的预算里。
argument-hint / arguments:前者是 / 菜单自动补全里显示的提示(如 [from-tag] [to-tag]),后者把位置参数映射成具名变量,这样正文里就能用 $from、$to 而不是 $0、$1。
allowed-tools:让 Claude 在本 Skill 激活期间,免确认地运行列出的工具。注意它只是免确认,不限制可用范围——你的权限设置仍对未列出工具生效。Bash(git log *) 这种带通配的写法,把授权收敛到具体子命令,避免给整个 Bash 开绿灯。
paths(可选,很关键):Glob 模式,只有当你正在处理匹配文件时,Claude 才会自动加载这个 Skill。对 release-notes 这种其实不太依赖路径的 Skill,这个字段意义不大;但对"只在前端代码里生效的命名规范 Skill",paths: "apps/web/**" 能极大减少误触发。本例加上是为了演示——它会让 Skill 在你编辑 src/ 下文件或 CHANGELOG.md 时更易被自动选中。
这一行还顺带演示了一个常见取舍:
allowed-tools写在项目级.claude/skills/时,要等你通过工作区信任对话框后才生效,和.claude/settings.json里的权限规则一致。提交进仓库的 Skill 等于把自己的工具授权写进团队代码,信任仓库前务必先看一遍。
正文是 Claude 触发后真正读到的指令。这里的杀手锏是动态上下文注入:用 !`命令` 语法,让 Claude Code 在把正文交给 Claude 之前先执行命令,再用输出替换占位符。Claude 收到的是真实数据,不是命令本身。
## 当前发版范围
从 `$from` 到 `$to`(若未给参数,默认取最近两个 tag)。
## 提交历史(已自动拉取)
!`git log $from..$to --pretty=format:"- %s" --no-merges 2>/dev/null || git log --pretty=format:"- %s" --no-merges -20`
## 你的任务
1. 读取 `reference.md` 里的 Conventional Commits 分类规则(仅在需要分类细节时读)。
2. 按规则把上面的提交归类到:✨ 新功能 / 🐛 修复 / ⚠️ 破坏性变更 / ♻️ 重构 / 📦 其他。
3. 严格照 `template.md` 的结构产出发布说明。
4. 若检测到 `BREAKING CHANGE` 或 `!:` 提交,在顶部用醒目方式列出迁移提示。
几条注入语法要点,踩过的人才懂:
!`cmd` 只有在 ! 出现在行首或紧跟空白时才被识别。写成 KEY=!cmd`` 这种紧贴前导字符的形式,Claude Code 会把它当普通文本,命令不会执行。```! 围栏代码块,而不是内联。!`...` 不会再展开。$from/$to 是位置具名变量(来自 arguments),会在 shell 执行之前被替换;若用户没传参就是空串。这就是上面 2>/dev/null || ... 兜底逻辑存在的原因。把所有规则塞进 SKILL.md 是第二个常见错误——一旦 Skill 被触发,正文会整段常驻上下文直到会话结束,每行都是反复燃烧的 token。官方建议 SKILL.md 控制在 500 行以内,长内容外移到支撑文件,只在用到时才被读。
reference.md(只在需要分类细节时加载):
# Conventional Commits 分类速查
- `feat:` / `feat(scope):` → 新功能 ✨
- `fix:` → 修复 🐛
- 后缀 `!` 或 footer `BREAKING CHANGE:` → 破坏性变更 ⚠️(必须置顶并附迁移说明)
- `refactor:` → 重构 ♻️(不改变外部行为)
- `docs:`,`chore:`,`test:`,`ci:`,`perf:` → 归到"其他 / 内务 📦"
- 中文提交信息(无前缀):按语义归类,拿不准归"其他"。
template.md(输出骨架):
# v<版本号> 发布说明
> 发布日期:<YYYY-MM-DD>
## ⚠️ 破坏性变更(如有)
-
## ✨ 新功能
-
## 🐛 修复
-
## 📦 其他
-
关键是在 SKILL.md 里显式引用这些文件,否则 Claude 不知道它们存在、何时该读:
## 资源
- 完整分类规则见 [reference.md](reference.md)
- 输出结构照 [template.md](template.md)
- 期望格式见 [examples/sample.md](examples/sample.md)
当分类逻辑复杂到写在 prompt 里又啰嗦又不可靠时,就外移成脚本。Skill 能捆绑任意语言的脚本。用 ${CLAUDE_SKILL_DIR} 引用脚本路径,它指向当前 SKILL.md 所在目录,无论 Skill 装在个人级、项目级还是插件级都能正确解析。
scripts/categorize.sh:
#!/usr/bin/env bash
# 读取 git log,按 Conventional Commits 归类计数
set -euo pipefail
FROM="${1:-}"; TO="${2:-HEAD}"
range="${FROM:+$FROM..}$TO"
git log $range --pretty=format:"%s" --no-merges 2>/dev/null \
| awk '
/^feat(\(.+\))?!?:/ {feat++; next}
/^fix(\(.+\))?!?:/ {fix++; next}
/^refactor:/ {ref++; next}
/!/ {brk++; next}
{other++}
END {
printf "✨ 新功能:%d\n🐛 修复:%d\n♻️ 重构:%d\n⚠️ 破坏性:%d\n📦 其他:%d\n", \
feat, fix, ref, brk, other
}'
在 SKILL.md 里这样调用(把它作为给 Claude 的"可选项",而非强制):
## 可选:先跑分类统计
在动笔前,可先运行统计,心里有数:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/categorize.sh" "$from" "$to"
> 给脚本加 `chmod +x` 并不强制(这里用 `bash xxx.sh` 调用),但养成习惯更好。脚本设计原则:**把确定性的脏活交给代码,把判断性的表达交给 Claude**——awk 负责数数,Claude 负责遣词造句和写迁移说明。
## 第 7 步:用 frontmatter 开关,精确控制"谁、在哪、怎么跑"
到这一步你的 Skill 已经能用了。剩下的 frontmatter 字段,是用来回答三个问题的:
**问题一:该你手动触发,还是让 Claude 自动触发?**
| 字段设置 | 你能 `/调用` | Claude 能自动触发 | 描述是否常驻上下文 |
| :--- | :--- | :--- | :--- |
| 默认 | 是 | 是 | 是 |
| `disable-model-invocation: true` | 是 | **否** | **否**(更省 token) |
| `user-invocable: false` | **否** | 是 | 是 |
`release-notes` 这类有明确"发布"动作、最好由人拍板时机的流程,适合加 `disable-model-invocation: true`,避免 Claude 看你 push 了就自作主张写发布说明。反过来,像"遗留系统背景知识"这种用户不会主动 `/`、却希望 Claude 在相关时自己想起来的,就该 `user-invocable: false`。
**问题二:要不要跑在隔离子代理里?**
加 `context: fork`(可配 `agent: Explore`)让 Skill 在独立上下文跑,`SKILL.md` 正文成为驱动子代理的 prompt。好处是不污染主对话、上下文干净;代价是**它看不到你的对话历史**。所以 `context: fork` 只适合"有明确任务步骤"的 Skill——纯指南型 Skill(只有约定没有任务)交给子代理,它会因为拿不到可执行动作而无功而返。
**问题三:要不要临时切模型/切算力?**
`model:` 可在 Skill 激活期间临时换模型(下一轮 prompt 自动恢复);`effort:` 可临时调档(`low/medium/high/xhigh/max`)。比如让 `release-notes` 用 `effort: low` 快速出一版草稿,省时省 token。
此外还有 `disallowed-tools`(激活期间**移除**某些工具,如让无人值守 Skill 不能调 `AskUserQuestion`)、`hooks`(绑定本 Skill 生命周期的钩子)、`shell: powershell`(Windows 下内联命令走 PowerShell)。这些字段官方文档列出有十余个,本教程挑了对触发与行为影响最大的几个讲透。
## 第 8 步:验证——别把"它动了"当成"它对了"
这是最被忽略、却最该做的一步。官方原话很到位:**"看到 Skill 被触发,只说明 Claude 找到了它,不说明它做到了你想要的。"**
按顺序做三件事:
**1. 确认它被发现且能被触发。**
What skills are available?
Claude 会列出当前可用 Skill。确认 `release-notes` 在列、描述没被截断。然后故意用**自然语言**(不是 `/release-notes`)测自动触发:
上个版本到现在都改了啥,整理成发布说明
如果 Claude 自动用了这个 Skill,说明 `description`/`when_to_use` 的关键词命中了。
**2. 用 `/doctor` 查描述预算。**
Skill 越多,描述越可能被砍。预算是**模型上下文窗口的 1%**,超了就**从你最不常调用的 Skill 开始砍描述**。`/doctor` 会直接告诉你哪些 Skill 的描述被缩短或丢弃。需要的话:
- 调大预算:`settings.json` 里设 `"skillListingBudgetFraction": 0.02`(2%),或环境变量 `SLASH_COMMAND_TOOL_CHAR_BUDGET` 设固定字符数;
- 给低优先 Skill 瘦身:`skillOverrides` 里把不常用的设成 `"name-only"`,只列名不占描述预算。
**3. 做"开/关"基线对照(判断质量,而非触发)。**
收集几条真实请求,**在全新会话**里分别跑两次:一次 Skill 可用、一次用 `skillOverrides` 设成 `"off"` 关掉,对比产出。为什么强调全新会话?因为你刚写完 Skill 时,作者上下文会掩盖指令的漏洞——同一个会话里 Claude "记得"你的意图,换一个干净会话才暴露真实差距。想做系统化对照,装官方 `skill-creator` 插件:
/plugin install skill-creator@claude-plugins-official /reload-plugins
让它跑隔离的测试用例、打分、出 benchmark,甚至做两个版本的盲测 A/B,确认你的改动是真改进才提交。
> 改了 `SKILL.md` 不用重启——Claude Code 会**实时监听** Skill 目录变化,保存即生效(仅限文本;若 Skill 同时是插件,改了 `hooks/`、`.mcp.json` 等要 `/reload-plugins`)。
## 第 9 步:分发——从个人到团队到全组织
按受众选分发方式:
- **项目级**:把 `.claude/skills/release-notes/` 提交进版本控制,团队 clone 即得;
- **插件**:在仓库根加 `.claude-plugin/plugin.json`,就能以插件形式打包,顺带带上 agents、hooks、MCP server;
- **托管级**:通过 managed settings 全组织下发,用户无法覆盖。
插件 Skill 用 `plugin-name:skill-name` 命名空间,天然不会和其他层冲突。同名优先级是 **enterprise > personal > project**,任意一层都会盖掉同名内置 Skill(你在项目 `.claude/skills/` 放个 `code-review`,就会替掉内置的 `/code-review`)。
---
## 实战案例:完整可运行的 `release-notes` Skill
把前面零散的片段拼成最终产物。四个文件,复制即可跑(前提是仓库有 git 历史和 Conventional Commits 风格的提交)。
**`~/.claude/skills/release-notes/SKILL.md`**:
```yaml
---
name: release-notes
description: 为当前或指定版本生成发布说明(release notes / changelog)。当用户询问"这次发版改了什么""上一次 tag 到现在有哪些变更""帮我写 v2.3.0 的更新日志"时使用。
when_to_use: |
触发短语:
- "帮我写这次发布的 release notes"
- "上个版本到现在改了什么"
- "为 v2.3.0 生成更新日志"
- "整理这周的 commit 做发布"
argument-hint: "[from-tag] [to-tag]"
arguments: [from, to]
allowed-tools: Bash(git log *) Bash(git tag *) Bash(bash *)
disable-model-invocation: false
---
## 范围
从 `$from` 到 `$to`;若用户未给参数,默认取最近两个 tag 之间。
## 提交历史(已自动拉取)
!`git log $from..$to --pretty=format:"- %s" --no-merges 2>/dev/null || git log --pretty=format:"- %s" --no-merges -30`
## 可选:先看分类统计
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/categorize.sh" "$from" "$to"
BREAKING CHANGE 或 !: 时,顶部用醒目方式列迁移提示。**`template.md`**、**`reference.md`**、**`scripts/categorize.sh`** 见第 4–6 步,原样落入目录即可。
**预期效果**:在一个有提交历史的仓库里输入"把 v1.2.0 到现在的改动整理成发布说明",Claude 自动触发本 Skill,先 `git log` 拉真实提交,跑一遍分类统计,再照模板输出带 emoji 分组、破坏性变更置顶的发布说明。若没自动触发,直接 `/release-notes v1.2.0 HEAD` 手动调用。
---
## 工作流全景图
下面这张图把"从用户输入到 Skill 正文加载"的完整生命周期画清楚了,也是排查触发问题的思维地图:
```mermaid
flowchart TD
A[用户输入一句话] --> B{Skill 描述预算内?}
B -- 否:被砍/丢弃 --> X[/doctor 查看<br/>调 skillListingBudgetFraction/]
B -- 是:描述常驻 --> C[Claude 用描述+when_to_use 匹配]
C --> D{命中且未被禁?}
D -- disable-model-invocation:true --> E[仅 /手动调用可达]
D -- paths 限定 --> F{正在编辑匹配文件?}
F -- 否 --> G[不自动加载]
F -- 是 --> H[触发:加载 SKILL.md 正文]
D -- 默认 --> H
H --> I[执行 !动态注入:git log 等]
I --> J{context: fork?}
J -- 是 --> K[在隔离子代理跑<br/>看不到主对话历史]
J -- 否 --> L[正文常驻主上下文<br/>直到会话结束/压缩]
K --> M[按正文产出发布说明]
L --> M
M --> N[对照无 Skill 基线<br/>验证产出质量]
| 症状 | 根因 | 解决 |
|---|---|---|
| 写完 Skill,Claude 不自动调用 | description 关键词和用户原话对不上 |
把用户真实说法写进 when_to_use;关键词前置 |
| 描述总被截断 | 超 1536 字符或预算不够 | 精简描述;/doctor 查;调 skillListingBudgetFraction |
/release-notes 还是旧名字 |
以为改 name 就改命令名 |
命令名来自目录名,改目录名才生效 |
!`git log` 没执行 |
! 紧跟前导字符 |
! 必须在行首或紧跟空白;多行用 ```! |
| 误触发(不该用却用了) | 描述太泛 | 收窄描述;加 paths 限定;或 disable-model-invocation: true |
项目级 allowed-tools 不生效 |
没过工作区信任对话框 | 先信任仓库目录;信任前审一遍 Skill 授的工具 |
| 改了文件没生效 | 新建了会话启动时不存在的顶级目录 | 重启 Claude Code;改已有文件则自动热加载 |
| 触发了但产出不对 | 缺基线对照 | 新会话里开/关各跑一次,或用 skill-creator 跑 eval |
description+when_to_use+动态注入+一个支撑文件的 Skill,并附 /doctor 截图证明其描述未被截断。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
