简介
Claude Code的本质是"大模型+工具调用循环+有限上下文窗口",与本章相关的限制是最后一项:凡常驻注入的内容,都会在每一轮对话中持续占用上下文、持续付出token成本。《2.Memory》解决了"指令会丢"的问题,但CLAUDE.md是启动时全量加载的——无论当前任务是否用得上,里面的每一节都躺在上下文里。
对"项目用什么构建命令"这类短事实,常驻是划算的。但另一类指令不一样:如何做一次代码审查、如何生成发布说明、如何迁移一个组件——它们是成段的工作流,只在特定任务时才需要。把工作流塞进CLAUDE.md,代价是每轮都为它付费,还稀释了真正需要常驻的规则。
Claude Code对此的解法是Agent Skills。Skill(技能)是调用时才加载的指令包:平时参与匹配的只有几行描述,成段的正文在被调用的那一刻才进入上下文。本章我们依次讨论Skill的结构、存放位置与发现、两种调用方式、front matter字段、参数与动态注入,最后给出三个可直接使用的示例。
Skill是什么
一个Skill就是一个目录,目录名即命令名,核心是其中的SKILL.md文件。SKILL.md分两部分:
- YAML front matter
文件顶部的元信息:名字、描述、触发条件、参数、工具权限等,供Claude Code决策"要不要用这个Skill"。 - Markdown正文
真正的指令内容:步骤、清单、格式要求。只在Skill被调用时才注入上下文。
这个两段式结构就是按需加载的实现。会话中常驻的只有描述那几十个字,正文躺在磁盘上,调用才计费。与CLAUDE.md"每字常驻"相比,这是另一种成本模型:CLAUDE.md为"每轮都可能用到"的内容优化,Skill为"偶尔用到、一用就是一整套"的内容优化。
什么时候该建Skill,有三个信号:
- 重复粘贴
同一段指令,我们已经在不同会话里手动贴过不止一次。 - 多步骤清单
某件事有固定的检查清单或操作顺序,希望每次执行都不走样。 - CLAUDE.md某节长成了流程
原本一两行的约定膨胀成"第一步……第二步……"。事实该常驻,流程该按需。
与旧式自定义命令的关系
在Skills之前,Claude Code的自定义斜杠命令放在.claude/commands/目录,一个Markdown文件对应一条命令,如.claude/commands/deploy.md对应/deploy。这一形态仍然兼容:旧文件不做任何修改也能继续使用;新旧同名时,Skill的优先级高于旧式命令。
两者的能力并不对等。旧式命令只是一个纯文本文件;Skill是一个目录,除SKILL.md外还能容纳辅助文件(supporting files),并且支持按语义自动触发。迁移路径很直接:把commands/name.md改名为skills/name/SKILL.md即可,功能等价,之后再按需添加辅助文件。
存放位置与发现
Skill可以放在四个层级,前三个层级同名时优先级从高到低;Plugin的Skill以命名空间隔离,不参与同名覆盖:
| 层级 | 位置 | 作用范围 | 优先级 |
|---|---|---|---|
| Enterprise | Managed设置(组织策略下发) | 组织内全部用户 | 最高 |
| Personal | ~/.claude/skills/<skill-name>/SKILL.md |
当前用户的所有项目 | 中 |
| Project | .claude/skills/<skill-name>/SKILL.md |
当前项目 | 低 |
| Plugin | 插件内的skills/目录 |
插件启用范围 | 命名空间隔离 |
插件这一行只需要知道"Skill也可以打包进插件分发",机制见《9.Plugins》。
发现是自动的。Claude Code会扫描项目根目录及其父目录下的.claude/skills/,也会扫描当前工作子目录下的.claude/skills/——后者让monorepo中的每个子项目都能带自己的Skill。子目录与上层出现同名Skill时不互相覆盖,两个都可用,嵌套的那个显示为dir:skill-name以作区分。
Skill支持热加载:编辑SKILL.md后,改动在当前会话内立即生效,不需要重启。
注意:即时生效仅限SKILL.md本身,hooks、agents等其他组件的改动不在此列。
调用方式
Skill有两条激活通道,两条通道都支持参数传递。
第一条是手动调用。目录名就是命令名,输入/skill-name即加载执行,可以带参数,如/fix-issue 123。
第二条是自动触发。Claude根据front matter中description与when_to_use的语义,判断当前任务是否与某个Skill相关,命中即自动加载。语义匹配只看这两个字段,Markdown正文不参与匹配——唯一的例外是省略description时,正文首段会被当作默认描述。若还设置了paths,则附加一个路径条件:只有在处理匹配路径的文件时才会自动加载(字段详情见下一节)。这条通道只在disable-model-invocation为false(默认值)时开放。
两条通道各有一个开关:
disable-model-invocation
设为true时关闭自动触发:该Skill不会被自动加载,也不会被预加载(Sub-Agent经skills字段预加载Skill的机制见《4.SubAgent》),只能由用户手动/skill-name调用。适合有副作用的动作类Skill——提交、部署、发通知,这类事应当由人显式发起,而不是由模型自行决定。user-invocable
设为false时该Skill从/菜单中隐藏,用户无法手动调用,但Claude仍可自动加载。适合纯知识型的"后台手册",既不占命令菜单,又保留自动出场的能力。
默认配置下两个开关都不启用,Skill对用户和模型双向开放;两个开关各自关闭其中一条通道,互不影响。
front matter字段
SKILL.md的front matter字段按用途分四组,完整清单如下:
| 分组 | 字段 | 说明 |
|---|---|---|
| 标识与触发 | name |
显示名称,不改变/命令名(仅插件根级Skill例外) |
| 标识与触发 | description |
自动触发的决策信号;省略时取正文首段 |
| 标识与触发 | when_to_use |
补充触发条件(触发词、示例请求),与description合并截断 |
| 标识与触发 | paths |
Glob路径模式,仅匹配文件时自动触发 |
| 标识与触发 | disable-model-invocation |
true时仅可手动调用(见上一节) |
| 标识与触发 | user-invocable |
false时从/菜单隐藏(见上一节) |
| 参数 | argument-hint |
参数自动完成提示,如[issue-number]、[filename] [format] |
| 参数 | arguments |
声明位置参数名,空格分隔或YAML列表,正文以$name或$0/$1引用 |
| 工具权限 | allowed-tools |
Skill活跃期间无需权限提示的工具列表,支持模式匹配 |
| 工具权限 | disallowed-tools |
Skill活跃期间移除的工具,下一条消息后限制清除 |
| 执行环境 | model |
覆盖此Skill使用的模型,inherit表示沿用会话当前模型 |
| 执行环境 | effort |
覆盖努力级别,取值low/medium/high/xhigh/max |
| 执行环境 | shell |
命令块使用的shell,bash(默认)或powershell |
| 执行环境 | context |
设为fork时在隔离的上下文中运行 |
| 执行环境 | agent |
context: fork时使用的Sub-Agent类型 |
| 执行环境 | hooks |
Skill生命周期内生效的Hook配置 |
三点补充:
allowed-tools与disallowed-tools的取值沿用权限规则的Tool(specifier)语法,语法本身见《1.概述》;两个字段只作用于当前Skill的作用域。context: fork会让Skill正文在一个隔离的Sub-Agent(子智能体)上下文中执行,agent指定所用的Sub-Agent类型,隔离执行的机制见《4.SubAgent》。hooks可在Skill激活期间挂载Hook(钩子),事件与协议见《5.Hooks》。
参数与动态上下文
Skill正文不是静态文本,它支持两类替换:调用参数与命令输出。
参数占位符如下:
| 占位符 | 含义 |
|---|---|
$ARGUMENTS |
用户输入的整段参数字符串 |
$ARGUMENTS[N]或$N |
第N个位置参数,从0开始计数 |
$name |
经arguments字段声明的命名参数 |
例如/migrate foo React Vue,则$0为foo、$1为React、$2为Vue。
若正文未包含$ARGUMENTS而用户传了参数,Claude Code会自动把ARGUMENTS: <input>追加到内容末尾,参数不会丢失。
动态上下文注入是另一类替换:正文中的!`<shell-command>`会在内容发送给模型之前先执行,命令输出原地替换占位符;多行命令也可以用带!标记的代码块承载。所用shell由shell字段决定。典型用法是注入!`git diff HEAD`——模型看到Skill正文时,里面已经是发送那一刻的真实diff,省去"先跑个命令看看"的一轮往返。
路径占位符${CLAUDE_PROJECT_DIR}展开为项目根目录的绝对路径(v2.1.196+),既可以出现在正文中,也可以用在allowed-tools的规则里,例如Bash(${CLAUDE_PROJECT_DIR}/scripts/lint.sh *)。
示例
示例代码:手动触发的动作类Skill,按编号修复GitHub issue。文件位于~/.claude/skills/fix-issue/SKILL.md:
1 | --- |
调用:/fix-issue 123。修issue会改代码、提交仓库,属于有副作用的动作,所以设disable-model-invocation: true,保证流程只由我们显式发起。
示例代码:自动触发加动态注入,总结未提交的改动。文件位于~/.claude/skills/summarize-changes/SKILL.md:
1 | --- |
这一篇没有写name,命令名取目录名summarize-changes。description写明了触发场景(use when asked what changed),当我们问"改了什么"时它会被自动加载;!`git diff HEAD`保证注入的是发送那一刻的真实diff,模型不需要再自己去跑命令。
示例代码:位置参数加工具权限,跨框架迁移组件。文件位于项目内.claude/skills/migrate-component/SKILL.md:
1 | --- |
调用:/migrate-component SearchBar React Vue,三个位置参数依次绑定到$component、$from、$to。allowed-tools让Bash(mv *)、Read、Edit这三类操作在Skill活跃期间免于权限提示——迁移只需要移动文件和改代码,就只放行这些。
经验
- 描述有长度上限
description与when_to_use合计上限1536字符,超出即被截断,过长的描述等于失效。触发词写精炼,不要把正文内容搬进描述。 - YAML错误静默降级
front matter有语法错误时,/skill-name手动调用仍然可用,但描述丢失、自动触发失效,且没有报错。怀疑Skill"失灵"时,用--debug启动查看解析错误。 - 该触发没触发,先查描述
正文写得再好也不参与语义匹配。触发场景要写进description或when_to_use,否则Skill不会被自动想起。 paths只管自动触发
路径不匹配只是不自动加载,手动/skill-name不受paths限制。$N越界原样保留
参数不足时$N不被替换、原样留在文本里。在argument-hint里写清参数格式,或在正文中交代参数缺省时怎么办。- 权限字段的时效
allowed-tools只在Skill活跃期间生效,disallowed-tools的限制在下一条消息后清除,两者都只作用于当前Skill。
与CLAUDE.md的分工可以归结为一句:事实常驻,流程按需。构建命令、代码约定这类每轮都可能用到的短事实留在CLAUDE.md;成段的工作流搬进Skill,只在调用的那一刻付token。
最后是Skills的边界。按需加载省下的是注入成本,但Skill被调用之后,工具调用循环(agent loop)仍然在当前这个上下文窗口里推进——读文件、跑命令产生的全部中间产物照样堆进主上下文。要把高噪声的活隔离出去,需要独立的上下文窗口,这是下一章《4.SubAgent》的主题。