本教程带你从零构建一个真实可用、可分发的 Claude Code 自定义 Skill——release-notes。它会读取一个 Git 仓库的 Conventional Commits 历史,自动归类、生成结构化的 CHANGELOG.md。你将掌握 Skill 的三层目录结构、YAML frontmatter 的每一个关键字段(尤其是触发词 description 与安全围栏 allowed-tools)、渐进式披露(progressive disclosure)设计,以及用 -p 无头模式做回归测试的工程化方法。适用人群:已跑通 Claude Code 基础流程、想从"写一段提示词"升级到"做一个可复用工具"的开发者。
claude --version 能输出版本号)在动手前,先建立正确的心智模型。一个成熟的 Skill 不是一个大文件,而是一个文件夹,由三层组成:
flowchart LR
A[Skill 文件夹<br/>release-notes/] --> B[SKILL.md<br/>入口与契约]
A --> C[scripts/<br/>可执行脚本]
A --> D[references/<br/>按需加载的参考资料]
B -->|第一阶段| E[模型发现层<br/>只读 frontmatter]
B -->|第二阶段| F[模型决策层<br/>读 SKILL.md 正文]
D -->|第三阶段| G[深度展开层<br/>按需 Read 引用文件]
C -.调用.-> G
这个分层有明确的目的:控制上下文成本。模型在一次会话里要面对成千上万个潜在 Skill,它不可能把每个 Skill 的全部内容都加载进来。Claude Code 的加载策略是分阶段的:
name 和 description 注入上下文(通常每个几十 token),用于判断"要不要激活"。SKILL.md 正文,理解工作流。references/xxx.md 或 scripts/xxx.py,模型才会用 Read 工具按需加载。这就是渐进式披露的核心思想——把"必须先知道的"放前面,把"可能用到的"留到后面按需加载。理解了这一点,你就理解了下面所有设计的"为什么"。
Claude Code 自带 skill-creator 技能,可以一键生成符合规范的骨架。在交互模式下直接说:
/skill-creator 创建一个名为 release-notes 的 skill,用途是从 git 历史生成 changelog
但为了吃透每个字段,本教程我们手工搭建,这样你能看清每一块的作用。先建目录:
# 个人级 Skill(所有项目可用)
mkdir -p ~/.claude/skills/release-notes/scripts
mkdir -p ~/.claude/skills/release-notes/references
# 或项目级 Skill(仅当前仓库可用,更推荐用于与代码强绑定的 skill)
mkdir -p .claude/skills/release-notes/scripts
mkdir -p .claude/skills/release-notes/references
预期结果:得到如下目录树。
release-notes/
├── SKILL.md ← 入口文件,契约 + 指令
├── scripts/
│ └── parse_commits.py
└── references/
└── conventional-commits.md
常见问题:
| 问题 | 原因 | 解决 |
|---|---|---|
放在 ~/.claude/skills/ 却不触发 |
目录名与 frontmatter name 不一致 |
保持目录名 = name 字段 |
| 项目级 Skill 不生效 | 未在仓库根目录的 .claude/skills/ 下 |
用 pwd 确认在仓库根,且 Claude Code 从该目录启动 |
| Windows 下编辑后加载失败 | 文件头被存了 UTF-8 BOM | 用 sed -i '1s/^\xEF\xBB\xBF//' SKILL.md 去掉 BOM |
frontmatter 是 Skill 与运行时之间的契约。运行时严格解析它,却宽松解析正文。所以这里错一个字符,整个 Skill 就"沉默失活"——能加载,但永远不会被触发。
先看我们要写的完整 frontmatter,再逐字段拆解:
---
name: release-notes
description: "Generate a structured CHANGELOG.md from a repository's git history using the Conventional Commits spec. Group entries by type (feat/fix/perf/...), highlight breaking changes, and emit Markdown. Use when: the user asks to generate release notes, draft a changelog, summarize commits since a tag, or prep a version bump. Not for: analyzing a single commit or writing commit messages."
allowed-tools: Read, Write, Bash, Grep
argument-hint: "[<from-rev>] [--to <rev>] [--dry-run]"
tags:
- tool:git
- type:documentation
- lang:python
user-invokable: true
---
name(必填,唯一身份)/release-notes 斜杠命令的 slugdescription(必填,触发命中的关键)这是整个 Skill 最重要的一行。模型在发现阶段只能看到这个字段,它决定 Skill 是否会被激活。官方统计显示,公开目录里 100% 的 Skill 都填了它——因为不填根本无法被发现。
写好它的三条铁律:
Generate a structured CHANGELOG.md from ...)the user asks to generate release notes, draft a changelog ...)Not for: analyzing a single commit),避免被相似的 Skill 吞掉为什么用英文写 description? 因为模型做触发匹配时,英文关键词密度更高、命中更稳。正文可以中文,但 description 推荐英文。长度 200–800 字符为佳,写成散文而非要点列表。
allowed-tools(强烈推荐,安全围栏)这是 Skill 的权限边界。列出后,激活期间模型只能调用这些工具。release-notes 需要:读历史(Bash 跑 git log)、写文件(Write 生成 CHANGELOG)、读引用(Read)、搜内容(Grep)。所以:
allowed-tools: Read, Write, Bash, Grep
如果省略这个字段,Skill 会继承会话的全部工具——一个号称"只读仓库"的 Skill 理论上也能 rm -rf。这是公开目录里最被忽视的安全字段(统计显示不到 10% 的 Skill 填了)。即使最简单的只读 Skill,也至少写 allowed-tools: Read, Grep。
argument-hint(UX 细节)当用户敲 /release-notes 不带参数时,运行时会把这行原样显示作为帮助。像 man page 的概要行那样写:尖括号必填、方括号选填。
tags(可发现性)用 命名空间:值 形式,便于目录检索与去歧义。5–8 个为宜,超过 15 个就成关键词堆砌。
user-invokable(调用方式)true 表示既能被模型自动触发,也能被用户用 /release-notes 显式调用。设为 false 则只能由模型按 description 自动决策触发(适合"元技能"或编排型 Skill)。
frontmatter 的隐形陷阱:
--- 必须顶格(第 0 列),前面不能有空格---,否则解析器会一直读到下一个 YAML 文档标记,可能吞掉正文里的 YAML 代码块description 里只要含"冒号+空格",整个值就必须加引号(否则被解析成键值对)写完用这条命令自检 YAML 是否合法(不必进 Claude Code 就能验证):
python3 -c "import yaml,sys; d=open(sys.argv[1]).read().split('---')[1]; print(yaml.safe_load(d))" \
~/.claude/skills/release-notes/SKILL.md
预期结果:打印出一个字典,包含 name、description、allowed-tools 等键,且无报错。
scripts/parse_commits.py把"机械的、确定性的"工作交给脚本,把"需要判断的"工作留给模型——这是 Skill 工程化的基本原则。解析 Git 提交、正则匹配 Conventional Commits 是确定性任务,写成 Python 脚本既快又稳、且无幻觉。
scripts/parse_commits.py:
#!/usr/bin/env python3
"""Parse git log into Conventional Commits groups. Output: JSON to stdout.
Usage:
parse_commits.py [<from-rev>] # defaults to <from-rev>..HEAD
parse_commits.py v1.1.0 # commits since tag v1.1.0
parse_commits.py v1.1.0 HEAD # explicit range
"""
import subprocess, json, re, sys
# feat(api)!: add pagination -> type=feat, scope=api, breaking=True
SUBJECT_RE = re.compile(
r'^(?P<type>\w+)(?:\((?P<scope>[\w-]+)\))?(?P<breaking>!)?:\s*(?P<desc>.+)$'
)
def git(*args):
return subprocess.run(['git', *args], capture_output=True,
text=True, check=True).stdout
def main():
argv = sys.argv[1:]
since = argv[0] if argv else None
until = argv[1] if len(argv) > 1 else 'HEAD'
rng = f'{since}..{until}' if since else until
# %H hash, %s subject, %b body; \t and \x1e as separators
fmt = '%H%x09%s%x09%b%x1e'
out = git('log', f'--pretty={fmt}', rng)
records = [r for r in out.split('\x1e') if r.strip()]
groups, unknown = {}, []
for rec in records:
parts = rec.strip().split('\t')
if len(parts) < 2:
continue
h, subj, body = parts[0], parts[1], parts[2] if len(parts) > 2 else ''
m = SUBJECT_RE.match(subj)
breaking = 'BREAKING CHANGE' in body
if not m:
unknown.append({'hash': h[:7], 'subject': subj})
continue
t = m.group('type')
groups.setdefault(t, []).append({
'hash': h[:7], 'scope': m.group('scope') or '',
'desc': m.group('desc').strip(),
'breaking': breaking or bool(m.group('breaking')),
})
print(json.dumps({'range': rng, 'groups': groups,
'unknown': unknown, 'count': len(records)}, indent=2))
if __name__ == '__main__':
main()
赋予执行权限并单独测试:
chmod +x ~/.claude/skills/release-notes/scripts/parse_commits.py
cd /path/to/your-repo
~/.claude/skills/release-notes/scripts/parse_commits.py v1.1.0
预期结果(节选):
{
"range": "v1.1.0..HEAD",
"groups": {
"feat": [
{"hash": "a3f2c10", "scope": "auth", "desc": "add OAuth2 login", "breaking": false}
],
"fix": [
{"hash": "b71e098", "scope": "", "desc": "crash on empty cart", "breaking": false}
]
},
"unknown": [],
"count": 2
}
常见问题:
fatal: ambiguous argument:传入的 tag 不存在。先 git tag 确认,或用 commit hash 代替。unknown 列表很长:说明仓库没用 Conventional Commits。这是预期行为,正文会教模型如何兜底(见步骤五)。BREAKING CHANGE 但没被识别:检查 %b(body)是否被正确切分;某些提交工具把破坏性说明写在 footer。references/conventional-commits.md正文不会每次都讲清 Conventional Commits 的每个细节,但我们也不想在正文里塞一张大表污染上下文。把规范速查表放进 references/,让模型在需要时自己决定读不读。
references/conventional-commits.md(精简版):
# Conventional Commits 速查
## 提交格式
<type>[optional scope][!]: <description>
[optional body]
[optional footer(s)]
## 类型语义与 CHANGELOG 章节
| type | 章节 | 严重度 |
|----------|--------------------|---------|
| feat | Features | 高 |
| fix | Bug Fixes | 高 |
| perf | Performance | 中 |
| refactor | Refactor | 低/省略 |
| docs | Documentation | 低/省略 |
| test | Tests | 低/省略 |
| chore | Maintenance | 省略 |
## 破坏性变更
- subject 里 `type!:` 或 footer `BREAKING CHANGE:` 均算
- CHANGELOG 顶部独立 "⚠ Breaking Changes" 区块
## 版本号推断(SemVer)
- 含 breaking → MAJOR
- 含 feat → MINOR
- 仅 fix/perf → PATCH
注意:这份文件不会被自动加载。只有在 SKILL.md 正文里写了"参考 references/conventional-commits.md"并被模型用 Read 打开时,它才进入上下文。这就是渐进式披露省 token 的机制。
正文是激活后模型真正遵循的指令。好的正文遵循"确定性给脚本,判断性给规则"原则,并明确引用按需加载的参考资料。
SKILL.md 正文(紧接 frontmatter 之后):
# Release Notes Generator
根据 Git 历史生成结构化 CHANGELOG。
## 工作流
1. **确定范围**。若无参数,用 `git describe --tags --abbrev=0` 取最近 tag 作为起点;
若无任何 tag,则用仓库首个提交。
2. **解析提交**。运行脚本(确定性步骤,避免幻觉):
```bash
python3 scripts/parse_commits.py <from-rev>
```
得到 JSON:`{groups, unknown, count}`。
3. **归类渲染**。按 `references/conventional-commits.md` 中的类型→章节映射,
把 `groups` 渲染为 Markdown。`docs/test/chore` 默认折叠或省略。
4. **破坏性变更**。任何 `breaking: true` 顶部独立成 "⚠ Breaking Changes" 区块。
5. **兜底**。若 `unknown` 非空且占比 >30%,在 CHANGELOG 末尾附
"## Other Changes" 原样列出,并在回复中提醒用户提交规范不统一。
6. **写文件**。默认追加到仓库根的 `CHANGELOG.md` 顶部(新版本在上)。
带 `--dry-run` 时只打印不写盘。
## 输出格式
使用 Keep a Changelog 风格:`## [vX.Y.Z] - YYYY-MM-DD` 作版本标题,
下方分 `### Features` / `### Bug Fixes` 等。每条 `- <desc> (<hash>)`。
## 何时加载 references
当你不确定某 type 对应哪个章节、或需要推断版本号时,再 Read
`references/conventional-commits.md`。无需每次都读。
几个写正文的要点:
--dry-run 分支:让 Skill 安全可测,这点在步骤七的回归测试里很有用。个人级 Skill 放对目录后无需重启,Claude Code 会自动发现。在交互模式下验证:
> 帮我生成 v1.2.0 的发布日志,基于上个 tag
模型应自动触发 release-notes,你会看到它调用 Bash 跑 git describe、调用 parse_commits.py、必要时 Read references、最后 Write 到 CHANGELOG.md。
也可以显式触发:
> /release-notes v1.1.0 --dry-run
预期结果:终端打印出渲染好的 Markdown,但不写盘。
触发失败的排查清单:
| 症状 | 排查 |
|---|---|
| 模型完全没注意到 Skill | description 缺 "Use when:" 触发短语 |
| 触发了但报"无权限" | allowed-tools 漏了 Bash 或 Write |
| 触发了别的 Skill | 加 "Not for:" 排除近邻场景 |
argument-hint 不显示 |
确认 user-invokable: true |
-p 无头模式做回归测试Skill 写完一次对不算成功,要能反复验证。用 -p(print/无头)模式把"触发→执行→输出"自动化:
# 回归用例 1:dry-run 应只打印不写盘
claude -p "用 release-notes 基于 v1.1.0 生成 changelog,dry-run 模式" \
| grep -q "## \[v1.2.0\]" && echo "PASS: 渲染正确" || echo "FAIL"
# 回归用例 2:破坏性变更应出现在顶部区块
claude -p "用 release-notes v1.1.0..HEAD,确认 breaking 变更在最前" \
| grep -q "Breaking Changes" && echo "PASS" || echo "FAIL"
# 回归用例 3:未知提交占比高时应提醒
claude -p "用 release-notes(该仓库提交不规范)" \
| grep -qi "提交规范" && echo "PASS: 兜底生效" || echo "FAIL"
把这三条存成 ~/.claude/skills/release-notes/test.sh,每次改 Skill 跑一遍,这就是把"祈祷式开发"变成"工程化开发"的关键一步。
下面是一个完整可复现的端到端示例。
1. 准备示例仓库(若没有现成仓库):
mkdir demo-shop && cd demo-shop && git init -q
git config user.email "[email protected]"; git config user.name "Dev"
echo "# demo-shop" > README.md && git add . && git commit -qm "chore: init"
git tag v1.0.0
git commit --allow-empty -qm "feat(auth): add OAuth2 login"
git commit --allow-empty -qm "fix(cart): crash on empty cart"
git commit --allow-empty -qm "feat!(api): rename /users to /accounts
BREAKING CHANGE: endpoint renamed"
git commit --allow-empty -qm "docs: update readme"
2. 触发 Skill(在仓库目录下启动 claude):
> 基于 v1.0.0 生成 v1.2.0 的 CHANGELOG
3. 预期生成的 CHANGELOG.md:
## [v1.2.0] - 2026-06-26
### ⚠ Breaking Changes
- rename /users to /accounts (5e...)
### Features
- add OAuth2 login (a3f2c10)
### Bug Fixes
- crash on empty cart (b71e098)
### Other Changes
- docs: update readme (c9...)
注意模型自动:把 feat! 提到顶部、把 docs 折进 Other Changes、按类型分章。这正是脚本(确定性解析)+ 正文(规则渲染)分工的成果。
sequenceDiagram
participant U as 用户
participant C as Claude Code
participant S as SKILL.md
participant P as parse_commits.py
participant R as references/
participant F as CHANGELOG.md
U->>C: 生成 v1.2.0 changelog
C->>S: 发现阶段:读 description 命中
C->>S: 决策阶段:读正文工作流
C->>P: Bash 跑 parse_commits.py v1.0.0
P-->>C: 返回 JSON {groups,unknown}
C->>R: Read conventional-commits.md(按需)
R-->>C: 类型→章节映射表
C->>F: Write 渲染后的 Markdown
F-->>U: 展示生成的 CHANGELOG
template/ 骨架和 docx/pdf/pptx 等生产级 Skill 源码,是最好的"抄作业"对象。/plugin marketplace add anthropics/skills 的思路,把你的 Skill 打包成可分发 Plugin。allowed-tools 围栏、有回归脚本的 Skill,并在班级仓库发起一次 CHANGELOG PR 作为结业作业。description 触发词的打磨与"脚本/模型分工"的判断,而非语法本身。parse_commits.py 换成调用 GitHub MCP 读取 PR;或加 metadata.api_base 让 Skill 适配私有 GitLab——可作为高阶作业。template/ 骨架,含 SKILL.md 最小结构与 frontmatter 约定name/description/allowed-tools/argument-hint/tags 等运行时行为与 YAML 坑点[!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 等场景全覆盖。
