文章

【笔记】Claude Code Skill 的发现、展开与执行链路

【笔记】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[加载远端声明式正文]

这条链路里有两个不同层次:

  1. description 等元数据回答“什么时候值得调用”;
  2. 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] 的简写
$nameFront 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 展开阶段执行命令,并把输出替换回正文。当前实现仍会调用工具权限检查;权限不允许、命令失败或被中断时,整个命令展开会报错。

这带来两个结论:

  1. allowed-tools 可以参与这一步的命令授权,但不等于无条件绕过权限系统;
  2. 动态输出已成为给模型的输入,应当按不可信外部内容对待,避免把用户可控文本直接拼进命令。

5. 第三阶段:处理 @ 引用

5.1 @ 不是 Markdown include

正文展开完成后,Claude Code 会把文本交给 attachment 解析器。它识别文件、MCP resource 和 Agent 等引用,并额外生成附件消息。

所以 @path/to/file 的效果不是在字符串层面把文件内容拼进 SKILL.md,而是:

  1. Skill 正文仍作为一条元用户消息;
  2. attachment 系统识别其中的引用;
  3. 被引用资源以额外上下文消息加入本轮请求。

模型最终能同时看到指令和附件,但两者在消息结构上不是同一块文本。

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 相关权限至少有三层,不能混为一谈:

  1. 调用权限:模型是否可以调用这个 Skill,受 disable-model-invocation 和 SkillTool 规则影响;
  2. 展开权限:动态 Shell 在加载正文时能否执行;
  3. 工作权限:正文注入后,模型后续工具调用是否因 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. 复习索引

最后用五句话记住整套机制:

  1. Skill 首先是一份带可发现元数据的提示词程序;
  2. 用户 slash command 和模型 SkillTool 最终共用正文展开逻辑;
  3. 展开依次处理参数、内置变量、动态 Shell,再解析 @ 附件;
  4. inline 用新消息和上下文修改器驱动当前模型,fork 把正文交给子 Agent;
  5. 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 上下文准备。
本文由作者按照 CC BY 4.0 进行授权