【笔记】Claude Code Skill 的发现、展开与执行链路
Claude Code 的 Skill 看起来像一个工具,实际更接近一种“可发现、可参数化、可改变执行上下文的提示词程序”。理解它的关键,不是只看 SKILL.md 写法,而是把发现、选择、展开和运行分开。
本文的用户可见能力以 Claude Code 官方文档为准;执行细节来自 2026-08-01 本地 harness 源码快照。后者属于当前实现,不应当作稳定协议。
1. 先建立中心模型
一次 Skill 调用可以概括为六步:
flowchart LR
A[扫描 Skill 元数据] --> B[用户或模型选择]
B --> C[加载 SKILL.md 正文]
C --> D[参数、变量与动态内容展开]
D --> E[解析附件]
E --> F{执行上下文}
F -->|inline| G[注入当前对话]
F -->|fork| H[交给子 Agent]
F -->|remote| I[加载远端声明式正文]
这条链路里有两个不同层次:
description等元数据回答“什么时候值得调用”;SKILL.md正文回答“调用后应该怎样工作”。
因此,渐进式加载不是“模型从一开始读完所有 Skill”,而是先暴露紧凑索引,选中后才加载完整说明。
2. Skill、slash command 与 SkillTool 的关系
2.1 Skill 是带元数据的提示词程序
一个典型 Skill 目录至少包含:
1
2
3
4
5
my-skill/
├── SKILL.md
├── scripts/
├── references/
└── assets/
SKILL.md 的 Front Matter 声明名称、描述、参数提示、允许工具和执行上下文;正文描述工作流程。其他文件不必预先塞进上下文,可以由正文按需引用或读取。
2.2 slash command 是用户入口
用户输入 /my-skill foo 时,命令解析器定位同名 prompt command,并把 foo 作为原始参数传给正文展开逻辑。Skill 默认既可由用户调用,也可由模型调用;带副作用的工作流可以设置:
1
disable-model-invocation: true
这只阻止模型通过 SkillTool 主动调用,不妨碍用户手动输入 slash command。
2.3 SkillTool 是模型入口
模型不能仅靠输出 /my-skill 文本来可靠地改变运行时。它通过 SkillTool 提交结构化调用:
1
2
3
4
{
"skill": "my-skill",
"args": "foo"
}
SkillTool 负责校验名称、检查是否允许模型调用、执行权限判断,再选择 inline、fork 或特殊 remote 路径。
它与普通数据型 Tool 的差别在于:普通 Tool 主要把一个结果返回给模型;SkillTool 还会把新的指令消息注入后续上下文,并可能修改工具权限、模型或执行位置。
3. 第一阶段:发现与选择
3.1 发现阶段主要加载元数据
Claude Code 扫描可用 Skill 后,先为模型构造名称与描述索引。description 不只是展示文字,也决定模型能否正确识别触发时机。
一条有效描述至少要说清:
- 它解决什么问题;
- 用户通常会怎样表达这个意图;
- 哪些相似任务不属于它。
正文再精细,如果描述含糊,也可能根本进不了展开阶段。
3.2 用户选择与模型选择共用同一个命令对象
两条入口最终都落到 prompt command 的 getPromptForCommand(args, context)。差别主要发生在外围:
- 用户入口由 slash command 解析器发起;
- 模型入口先经过 SkillTool 的参数校验和权限系统;
disable-model-invocation只对后一条路径生效。
这也是为什么 Skill 同时具有“命令”和“工具”两种表象:它们是同一正文的不同触发方式。
4. 第二阶段:展开 SKILL.md
4.1 注入基准目录
本地 Skill 加载后,当前实现会在正文前追加:
1
Base directory for this skill: /absolute/path/to/my-skill
它告诉模型,相对引用应以 Skill 自身目录为基准,而不是当前项目目录。
正文中的 ${CLAUDE_SKILL_DIR} 也会替换成这个绝对目录,适合引用随 Skill 分发的脚本或模板。
4.2 参数替换
当前实现支持四种参数形式:
| 形式 | 含义 |
|---|---|
$ARGUMENTS | 完整原始参数字符串 |
$ARGUMENTS[0] | shell 风格解析后的第一个参数 |
$0 | $ARGUMENTS[0] 的简写 |
$name | Front Matter arguments 中对应位置的命名参数 |
例如:
1
2
3
4
5
6
7
---
arguments: target format
argument-hint: "[target] [format]"
---
整理 $target,并以 $format 输出。
原始输入:$ARGUMENTS
参数拆分使用 shell 风格引用规则,因此 foo "hello world" 会得到两个位置参数,而不是三个。变量语法会被保留为字面值,不在这里当作 shell 环境变量展开。
如果传入了非空参数,但正文没有任何占位符,当前本地实现会在末尾补上 ARGUMENTS: ...,避免参数静默丢失。这是实现行为,不宜依赖为跨版本契约。
4.3 内置变量替换
除 Skill 目录外,当前实现还替换 ${CLAUDE_SESSION_ID}。这些替换发生在动态 Shell 执行之前,所以 Shell 片段可以使用已经解析好的 Skill 路径。
4.4 动态 Shell 内容
本地 Skill 正文支持两种动态执行语法:
1
!`git status --short`
以及:
1
2
3
```!
git status --short
```
它们不是让模型稍后决定是否运行 Bash,而是在 prompt 展开阶段执行命令,并把输出替换回正文。当前实现仍会调用工具权限检查;权限不允许、命令失败或被中断时,整个命令展开会报错。
这带来两个结论:
allowed-tools可以参与这一步的命令授权,但不等于无条件绕过权限系统;- 动态输出已成为给模型的输入,应当按不可信外部内容对待,避免把用户可控文本直接拼进命令。
5. 第三阶段:处理 @ 引用
5.1 @ 不是 Markdown include
正文展开完成后,Claude Code 会把文本交给 attachment 解析器。它识别文件、MCP resource 和 Agent 等引用,并额外生成附件消息。
所以 @path/to/file 的效果不是在字符串层面把文件内容拼进 SKILL.md,而是:
- Skill 正文仍作为一条元用户消息;
- attachment 系统识别其中的引用;
- 被引用资源以额外上下文消息加入本轮请求。
模型最终能同时看到指令和附件,但两者在消息结构上不是同一块文本。
5.2 skipSkillDiscovery 只关闭递归发现
源码中的 skipSkillDiscovery: true 容易被误读成“不处理 Skill 正文里的 @”。实际恰好相反:attachment 解析仍然执行,只是禁止把已经加载的 SKILL.md 再当作用户意图去搜索其他 Skill。
这个边界是为了避免大段元内容触发重复发现,而不是关闭文件或资源附件。
5.3 引用仍受可解析性和权限约束
@ 只是一种引用信号,不保证目标一定存在、可读或会被完整加载。Skill 作者应优先使用明确路径,并用 ${CLAUDE_SKILL_DIR} 表达 Skill 自带资源的位置,避免依赖调用时工作目录。
6. 第四阶段:inline、fork 与 remote
6.1 inline:把工作流注入当前对话
inline 是默认模式。SkillTool 完成自身工具协议后,还返回 newMessages,把展开后的 Skill 正文注入当前会话;contextModifier 再把 allowed-tools、模型等设置带入后续运行。
sequenceDiagram
participant M as 当前模型
participant T as SkillTool
participant R as 外层工具运行器
M->>T: skill + args
T-->>R: data + newMessages + contextModifier
R-->>M: tool_result
R-->>M: 展开后的 Skill 消息
Note over M: 在当前上下文继续执行正文
它适合需要继承当前对话、连续使用已有上下文、并让主模型亲自完成的工作流。
6.2 为什么既有 tool_result 又有 Skill 消息
二者解决的是不同问题:
tool_result闭合模型已经发出的 tool call,满足工具调用协议;- Skill 消息把真正的工作说明放进后续上下文;
contextModifier改变执行这些说明时的运行环境。
只返回 Skill 正文会留下未闭合的 tool use;只返回普通结果又无法自然地让当前模型继续执行长工作流。
6.3 fork:在独立 Agent 上下文执行
设置 context: fork 后,当前实现先做同样的正文、参数和动态内容展开,再将结果作为子 Agent 的初始用户消息。子 Agent 使用指定的 agent,未指定时回退到通用 Agent,并获得该 Skill 声明的工具权限。
父会话等待的是子 Agent 的最终结果,而不是把完整 Skill 正文注入当前模型。它适合:
- 工作上下文很大,容易污染主会话;
- 任务可独立收敛为一个结果;
- 希望隔离中间推理和大量工具输出。
fork 隔离的是对话和部分运行状态,不天然等于文件系统隔离。子 Agent 若在同一工作区修改文件,仍可能影响父任务。
6.4 remote:当前源码中的特殊实验路径
本地 harness 还存在远端 canonical Skill 路径。它先发现远端元数据,调用时再下载并缓存正文,然后直接注入用户消息。
这条路径与普通本地 Skill 有意不同:
- 不执行正文中的动态 Shell;
- 不做
$ARGUMENTS插值; - 仍替换 Skill 目录和 session ID;
- 当前受实验特性和用户类型限制。
因此,不能把它概括成公开稳定的第三种通用执行模式。更准确的说法是:当前内部实现为远端声明式 Skill 提供了一条受限加载路径。
7. 权限到底分成几层
Skill 相关权限至少有三层,不能混为一谈:
- 调用权限:模型是否可以调用这个 Skill,受
disable-model-invocation和 SkillTool 规则影响; - 展开权限:动态 Shell 在加载正文时能否执行;
- 工作权限:正文注入后,模型后续工具调用是否因
allowed-tools获得额外允许规则。
allowed-tools 的作用是收窄或预授权既定工作流,不是把 Skill 变成可信代码。正文、参数、动态输出和附件仍可能引入不可信内容。
8. 一次 inline 调用的完整消息语义
把内部对象简化后,一次模型主动调用大致是:
1
2
3
4
5
6
assistant: tool_use Skill({ skill, args })
tool: tool_result { success, commandName, ... }
user(meta): 展开后的 SKILL.md 正文
user/meta: 由 @ 引用解析出的附件消息
context: 追加 allowed-tools,必要时切换模型
assistant: 按 Skill 正文继续执行
其中 user(meta) 是运行时注入的指令载体,并不表示真实用户又发送了一条消息。理解这一点,才能解释为什么 SkillTool 的返回值里既有普通结果,又有 newMessages。
9. 设计 Skill 时的实用判断
9.1 元数据只负责让它被正确选中
描述应短而有辨识度,不要把完整流程塞进 Front Matter。正文则应自包含地说明目标、边界、步骤和验收条件,因为真正执行时模型依赖的是展开后的正文。
9.2 参数用于传入意图,不用于拼接危险命令
优先把参数写进自然语言指令,让模型或受控脚本做校验。若参数进入动态 Shell,必须显式处理引用、允许值和目标范围,否则展开阶段就可能发生命令注入。
9.3 inline 与 fork 按上下文所有权选择
- 任务需要继承并继续塑造当前对话:选 inline;
- 任务能独立执行,只需把结果带回:选 fork;
- 任务会修改共享文件时,不要把 fork 误当成 worktree。
9.4 引用大型材料时保持渐进加载
把稳定规则留在 SKILL.md,把专题资料拆到 references/,在正文中说明何时读取。这样既降低初始上下文成本,也能让模型知道资源的选择条件。
10. 复习索引
最后用五句话记住整套机制:
- Skill 首先是一份带可发现元数据的提示词程序;
- 用户 slash command 和模型 SkillTool 最终共用正文展开逻辑;
- 展开依次处理参数、内置变量、动态 Shell,再解析
@附件; - inline 用新消息和上下文修改器驱动当前模型,fork 把正文交给子 Agent;
tool_result负责协议闭环,Skill 消息负责承载真正的工作流。
11. 核验入口
- Claude Code 官方 Skills 文档:用于核验目录格式、Front Matter、参数、
${CLAUDE_SKILL_DIR}、动态 Shell、inline 与 fork 等用户可见行为; src/skills/loadSkillsDir.ts:本地 Skill 加载、参数与变量替换、动态 Shell;src/utils/argumentSubstitution.ts:位置参数和命名参数解析;src/utils/processUserInput/processSlashCommand.tsx:正文消息、附件和权限消息组装;src/utils/attachments.ts:@引用与skipSkillDiscovery边界;src/tools/SkillTool/SkillTool.ts:模型调用、inline、fork 和 remote 分流;src/utils/forkedAgent.ts:fork 上下文准备。