引子
如何做一次代码审查、如何生成发布说明、如何迁移一个组件,这些工作只在特定任务时才需要。把这些塞进CLAUDE.md,不仅浪费token,还稀释了真正需要常驻的规则。
那么:只在特定任务时才需要的成段指令,怎样做到平时只留几行描述、调用那一刻才注入正文?
这就是Skill要解决的问题。
Skill,一种调用时才加载的指令包。
官方文档:https://code.claude.com/docs/en/skills
原理
理解Skill,有三个要点:
- 两段式结构怎样实现按需加载
- 两条调用通道怎么触发
- 按需加载的边界在哪
两段式结构:按需加载的实现
一个Skill就是一个目录,目录名即命令名,核心是其中的SKILL.md文件。
一份SKILL.md分两部分:
- YAML front matter:文件顶部的元信息(名字、描述、触发条件、参数、工具权限等),供ClaudeCode决策"要不要用这个Skill"。
- Markdown正文:真正的指令内容(步骤、清单、格式要求),只在Skill被调用时才注入上下文。
这个两段式就是"按需加载"的实现:会话中常驻的只有"YAML front matter"部分,正文躺在磁盘上,调用那一刻才进入上下文。
即,Skill帮我们省的是正文,不是描述。"要不要用"的判断靠描述做语义匹配,描述本身就必须常驻上下文,这部分成本省不掉;按需加载省下的只是正文的注入成本。
所以,触发词要写精炼,别把正文内容搬进描述。
两条调用通道
Skill有两条调用通道:
- 手动执行:输入
/skill-name(目录名即命令名)即加载执行,可带参数,如/fix-issue 123。 - 自动触发:Claude读取"YAML front matter"里的
description与when_to_use,基于语义判断当前任务是否与某个Skill相关,命中就自动加载。
自动触发的匹配只看description和when_to_use,Markdown正文不参与(唯一例外是没写description时,正文首段被当默认描述)。
所以,某个Skill"该触发却没触发"时,第一件事是检查描述,而不是改正文。
按需加载的边界
按需加载有一条边界:Skill被调用后,它的工具调用循环仍在主上下文里推进,读文件、跑命令的中间产物照样堆进主窗口。
也就是说,默认情况下Skill管的是"注入",管不了"执行"。要让执行也在隔离的上下文中进行,需要用context: fork字段——它会让Skill正文在一个独立的SubAgent中执行,过程不污染主上下文。细节见下一章《4.SubAgent》。
选型:什么时候该建Skill
一句话的判断标准:这段指令是"成段的流程",且"只在特定任务时才用到"。两条都占,就该把它收进Skill,而不是塞进CLAUDE.md。
这个标准落到日常,遇到下面任意一种情况,多半就该建Skill了:
- 重复粘贴
同一段指令,我们已经在不同会话里手动贴过不止一次。比如每开一个新会话,都要把"提交信息该怎么写"那套规矩重贴一遍,重复本身就是它该被固化下来的证据。 - 多步骤清单
某件事有固定的操作顺序或检查清单,希望每次都照同一套走、不走样。比如"发布新版本"固定是改版本号、更新changelog、打tag、推送这一串,漏一步就出岔子。 - CLAUDE.md某节长成了流程
原本一两行的约定,膨胀成了"第一步……第二步……"的操作手册。比如CLAUDE.md里"怎么加一个数据库迁移"从一句话堆成了七八步的清单,这正是《2.Memory》里"不写废话"要清出去的内容。
示例
这里提供三个真实的Skill,建立直观印象。它们各有一个"卖点":手动锁定、自动触发加动态注入、位置参数加工具权限。
实践
front matter字段
SKILL.md的front matter字段按用途分四组:
- 标识与触发
- 参数
- 工具权限
- 执行环境
标识与触发
| 字段 | 说明 |
|---|---|
name |
显示名称,不改变/命令名(仅插件根级Skill例外) |
description |
自动触发的决策信号;省略时取正文首段 |
when_to_use |
补充触发条件(触发词、示例请求),与description合并截断 |
paths |
Glob路径模式,仅匹配文件时自动触发 |
disable-model-invocation |
true时仅可手动调用 |
user-invocable |
false时从/菜单隐藏 |
description与when_to_use合计有 1536 字符上限,超出会被截断。即,过长的描述等于失效。paths在自动触发上再叠加一个路径条件:只有当前处理的文件匹配这些glob时才自动加载。注意paths只收窄自动触发这一条通道,路径不匹配仅仅是不自动加载,手动/skill-name调用不受影响。
参数
| 字段 | 说明 |
|---|---|
argument-hint |
参数自动完成提示,如[issue-number]、[filename] [format] |
arguments |
声明位置参数名,空格分隔或YAML列表,正文以$name或$0/$1引用 |
工具权限
| 字段 | 说明 |
|---|---|
allowed-tools |
Skill活跃期间无需权限提示的工具列表,支持模式匹配 |
disallowed-tools |
Skill活跃期间移除的工具,下一条消息后限制清除 |
allowed-tools与disallowed-tools的取值沿用权限规则的Tool(specifier)语法,语法本身见《1.Start》;两个字段只作用于当前Skill的作用域。- 与settings中的权限规则冲突时,禁止的一方生效。
allowed-tools免掉的只是权限提示,相当于Skill活跃期内的一条allow规则,按deny > ask > allow的优先级,压不过settings里的deny(ask同理,仍会弹提示);disallowed-tools则是把工具从可用池中直接移除,settings的allow只负责免提示,不能把被移除的工具加回来。
执行环境
| 字段 | 说明 |
|---|---|
model |
覆盖此Skill使用的模型,inherit表示沿用会话当前模型 |
effort |
覆盖努力级别,取值low/medium/high/xhigh/max |
shell |
命令块使用的shell,bash(默认)或powershell |
context |
设为fork时在隔离的上下文中运行 |
agent |
context: fork时使用的Sub-Agent类型 |
hooks |
Skill生命周期内生效的Hook配置 |
context: fork会让Skill正文在一个隔离的Sub-Agent(子智能体)上下文中执行agent指定所用的Sub-Agent类型。- 隔离执行的机制见《4.SubAgent》。
hooks可在Skill激活期间挂载Hook(钩子)。- 事件与协议见《5.Hook》。
存放位置及其调用名和规则
五个存放层级
Skill有五个存放层级,位置、作用范围与同名时的处理如下表。
| 层级 | 位置 | 作用范围 | 同名时 |
|---|---|---|---|
| Enterprise | Managed设置(组织策略下发) | 组织内全部用户 | 优先级最高 |
| Personal | ~/.claude/skills/<skill-name>/SKILL.md |
当前用户的所有项目 | 优先级中 |
| Project | 项目根目录 .claude/skills/<skill-name>/SKILL.md |
当前项目 | 优先级低 |
| 子目录 | 工作子目录下的 .claude/skills/<skill-name>/SKILL.md |
该子目录 | 撞名才加路径前缀,二者共存 |
| Plugin | 插件内的skills/目录 |
插件启用范围 | 永远带插件前缀,不撞名 |
调用名和规则
调用名有两种:
- 裸名(就是目录名,如
/deploy) - 带前缀(如
plugin-name:skill、apps/web:deploy)
具体规则三条:
- Enterprise、Personal、Project:都用裸名
/skill-name。只有这三类才会"撞名覆盖",同名时按表中优先级取一个生效。 - Plugin:调用名永远带插件名前缀,形如
plugin-name:skill-name。名字自带命名空间,跟别处的裸名永不相等。 - 子目录:平时也用裸名(目录名),只有当它和别处的Skill撞名时,才自动带上子目录路径前缀来消歧。
比如根目录已经有/deploy,子目录apps/web/下又放了个同名的,后者的调用名就变成/apps/web:deploy,于是两个都留得下、谁也不盖谁。
旧式自定义命令
旧式自定义命令是存放在.claude/commands/目录下的命令文件(插件内对应commands/目录)。新做法优先用Skill,插件内形态见《7.Plugin》。
动态上下文
Skill正文可以不是静态文本,而是一份模板,其中的占位符会在正文送入上下文之前先被求值、替换成实际内容。
可替换的有三类:
- 参数替换
- 动态命令注入
- 路径占位符
参数替换
调用Skill时跟在命令后面的参数,通过下列占位符注入正文:
| 占位符 | 含义 |
|---|---|
$ARGUMENTS |
用户传入的整段参数字符串 |
$N(等价于$ARGUMENTS[N]) |
第N个位置参数,从0开始计数 |
$name |
由front matter的arguments字段声明的命名参数 |
以/migrate foo React Vue为例,$0、$1、$2依次替换为foo、React、Vue。
两个边界情况要注意:
- 参数不足:越界的占位符不做替换、原样保留。上例只传了三个参数,正文里的
$3就会原封不动显示为$3。所以要在argument-hint里写清参数格式,或在正文里交代参数缺省时该怎么处理。 - 正文没写
$ARGUMENTS、用户却传了参数:参数不会丢失,ClaudeCode会把ARGUMENTS: <用户输入>自动追加到正文末尾。
动态命令注入
动态命令注入:把shell命令的输出嵌进正文。
即正文里的!`<shell命令>`会在正文发送给模型之前先执行,命令输出原地替换掉这个占位符。
多行命令如果塞在一行里可读性太差,可以改用代码块写法:三反引号的语言标记前加!,即```!bash(或直接```!),块内的命令照常书写。执行后,整个代码块同样被命令输出原地替换。例如:
1 | ```!bash |
命令由哪个shell执行,取决于front matter的shell字段(bash或powershell)。
路径占位符
路径占位符:正文里可以用${CLAUDE_PROJECT_DIR}引用项目根目录的绝对路径。
路径占位符不仅能出现在正文中,也能写进allowed-tools规则,例如Bash(${CLAUDE_PROJECT_DIR}/scripts/lint.sh *),据此按项目根定位脚本。
排障速查
一个前提
前提是立即生效:在编辑SKILL.md后,改动在当前会话内立即生效,不需要重启,可以边改边验证。
三个高频症状
- 第一个症状,“该触发却没触发”:先检查描述,口诀见前文「两条调用通道」。
- 第二个症状与YAML语法有关。对于"front matter"一旦有YAML语法错误,ClaudeCode不会报错,而是静默降级:
/skill-name手动调用仍然可用,但描述丢失、自动触发失效。
所以当一个Skill突然"只能手动、不再自动出场"时,优先怀疑front matter写错了。 - 第三个症状,“声明了
allowed-tools却仍被拦、仍弹提示”:先检查settings里是否有命中的deny或ask规则,冲突时禁止的一方生效,原因见前文「工具权限」。
关闭调用通道
「两条调用通道」默认都开放。要单独关掉某一条,各由一个字段控制、互不影响:user-invocable关手动,disable-model-invocation关自动。
user-invocable: false适合纯知识型的"后台手册":既不占命令菜单,又保留自动出场的能力。disable-model-invocation: true适合有副作用的动作类Skill,提交、部署、发通知,这类事应当由人显式发起,而非模型自行决定。
注意:设为true后,不光自动触发被关掉,连description(front matter)都不会注入上下文,这个Skill对ClaudeCode完全不可见,包括子智能体也不会预加载该Skill(参考《4.SubAgent》)。