前情提要
- Skill 是一个目录,核心是一份
SKILL.md:开头的 frontmatter 写name和description,正文写流程- Skill 最核心的设计是渐进式加载,分三层:
层级 加载什么 什么时候 第一层 name+description会话启动时 第二层 SKILL.md正文判断需要这个 Skill 时 第三层 模板、脚本、参考文档 真正用到时
- 常听到的一句话:「装 500 个 Skill,也只会把 name + description 加载到上下文」
- 斜杠指令和 Skill 已经合并成同一种机制;
disable-model-invocation可以让 Skill 只能手动/名字触发
这一节去源码里看三层分别是怎么实现的,顺便回答一个问题:装 500 个 Skill,真的没问题吗?
主要看两个地方:src/skills/loadSkillsDir.ts(从磁盘加载)和 SkillTool/(调用 Skill 的那个工具)。
一、Skill 和斜杠指令是一回事
loadSkillsDir.ts 会同时从这几个地方找 Skill:
| 来源 | 路径 |
|---|---|
| 组织级 | IT 统一下发目录下的 .claude/skills/ |
| 用户级 | ~/.claude/skills/ |
| 项目级 | 从当前目录往上找,每一层的 .claude/skills/ |
| 额外目录 | --add-dir 指定的目录下的 .claude/skills/ |
| 旧的指令目录 | .claude/commands/ |
注意最后一行:旧的 commands/ 目录,用的是同一个加载器。 从那里加载进来的东西,来源被标成了这个名字:
cmd.loadedFrom === 'commands_DEPRECATED'直接写着 DEPRECATED(已弃用)。
斜杠指令已经是 Skill 的一种,新写的流程放 skills/ 就对了。
二、frontmatter 里能写什么
loadSkillsDir.ts 会从 SKILL.md 开头的 frontmatter 里读这些字段:
| 字段 | 作用 | 在哪讲 |
|---|---|---|
name / description |
出现在 Skill 目录里 | 本节第三、四部分 |
when_to_use |
拼在 description 后面:描述 - 什么时候用 |
|
disable-model-invocation |
模型不能自己调用,只能你手动 /名字 |
本节第五部分 |
user-invocable |
设为 false 时,你不能用 /名字 调用 |
|
allowed-tools |
这个 Skill 运行时,这些工具自动放行 | 本节第五部分 |
argument-hint / arguments |
参数提示,正文里的 $ARGUMENTS 会被替换 |
|
model / effort |
运行这个 Skill 时换模型、换思考强度 | |
context: fork |
放到子代理里运行,只带回结果 | 本节第七部分 |
paths |
碰到匹配的文件才激活,和规则文件的写法一样 | |
hooks |
这个 Skill 运行期间生效的 Hook | 第9节 |
description 如果没写,从 skills/ 目录加载的 Skill 会拿正文第一行凑一个;但插件和 MCP 提供的 Skill 没写 description 的话,不会出现在目录里。
三、第一层:目录是一条附件

你可能以为 Skill 目录写在系统提示词里。其实不是。
它是一个叫 skill_listing 的附件,以一条 <system-reminder> 消息的形式插进对话:
<system-reminder>
The following skills are available for use with the Skill tool:
- weekly-report: 根据本周 git 提交生成周报。 - 当用户要求写周报、总结本周工作时使用。
- release: 发版流程……
- ……
</system-reminder>
而且只发新增的。源码里用一个集合记着「哪些 Skill 已经告诉过模型了」:会话开始时发一次全量,之后你中途又装了新 Skill,只补发新的那几个。
为什么这么麻烦?回想第5节:系统提示词不能中途改,改了缓存就作废;会话中的变化要以附件形式追加在末尾。Skill 目录就是这么做的。
Skill 目录不在系统提示词里,而是一条只补发新增项的附件。
四、目录有预算
关键在 SkillTool/prompt.ts 开头这几行:
// Skill listing gets 1% of the context window (in characters)
// Skill 目录只能占上下文窗口的 1%(按字符算)
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const CHARS_PER_TOKEN = 4
// v2.1.117: raised from 250 → 1536 to allow richer skill descriptions.
// v2.1.117:单条上限从 250 提高到 1536,允许写更丰富的描述
export const MAX_LISTING_DESC_CHARS = 1536两个限制:
| 限制 | 数值 | 说明 |
|---|---|---|
| 单条上限 | 1536 字符 | description 和 when_to_use 加起来,超出的截断 |
| 总预算 | 上下文窗口的 1% | 按 1 token ≈ 4 字符折算。200K 窗口 → 8,000 字符;1M 窗口 → 40,000 字符 |
超预算了怎么办
formatCommandsWithinBudget() 分三步处理:
1. 先试着放完整描述 → 放得下就全放
2. 放不下 → 内置的 Skill 保留完整描述;其余的平分剩下的预算,每条截到一样长
3. 平分之后每条连 20 个字符都不到 → 其余的 Skill 只留名字,描述全部删掉
算一笔账
200K 窗口,预算 8,000 字符。你装了 100 个 Skill,名字平均 15 个字符:
名字和格式本身就要占:100 × (15 + 4) + 99 ≈ 2,000 字符
剩给描述的: 8,000 − 2,000 = 6,000 字符
每条描述能分到: 6,000 ÷ 100 = 60 字符
这还没算内置 Skill 占掉的部分。也就是说,装到 100 个左右,每个 Skill 的描述就只剩一句话了;再多装,就只剩名字。
还有一个中文用户要注意的细节:在这份源码里,预算是按终端显示宽度算的,一个汉字算 2 个宽度。中文 description 吃预算的速度,是同样字数英文的两倍。