claude code book · 第三部分:高级模式篇 · 第 11 部分

第 11 章 技能系统与插件架构

理解技能系统的声明式工具定义,掌握插件生命周期的加载与运行机制

第11章 技能系统与插件架构

“好的架构让增加功能变简单,伟大的架构让移除功能也一样简单。” — 《架构整洁之道》改编

学习目标:

  • 掌握 Claude Code 技能(Skill)的定义、加载和执行机制的完整生命周期
  • 理解从用户级到管理级的技能分层策略及其安全含义
  • 学会创建自定义技能的完整流程和最佳实践
  • 了解插件系统的缓存优先加载模型和安全边界
  • 掌握技能组合使用的模式和反模式

11.1 技能系统架构

Claude Code 的技能系统是一个多层次的扩展机制。它允许用户通过 Markdown 文件定义可复用的 prompt 模板,也允许开发者通过 TypeScript 代码注册编译时内置的技能。整个系统的设计目标是:零配置可用,有配置强大

这就像一套厨师的工具箱:内置的刀具和锅具(内置技能)可以应对大部分烹饪需求,但当你需要制作特定菜系时,可以添加专用的工具(自定义技能)。而且,这些工具可以按层级管理——家庭厨房(用户级)、餐厅厨房(项目级)、连锁餐饮标准(管理级)。

11.1.1 内置技能清单

内置技能(Bundled Skills)是编译进 CLI 二进制文件的技能,对所有用户开箱即用。它们在启动时通过 initBundledSkills() 统一注册。

注册入口函数根据功能标志决定加载哪些技能。技能分为两类:

  • 核心技能:始终注册,如 verifydebugsimplifyrememberbatchstuck
  • Feature-gated 技能:通过功能门控,仅在对应功能标志启用时注册,如 loop(AGENT_TRIGGERS)、claude-api(BUILDING_CLAUDE_APPS)、schedule(AGENT_TRIGGERS_REMOTE)

核心内置技能的完整清单如下:

技能名称用途用户可调用典型使用场景
update-config配置 settings.json 的各项设置修改权限规则、环境变量、默认模型
keybindings-help键盘快捷键自定义帮助查看和自定义按键绑定
verify验证代码变更的正确性提交前的最终验证、CI 前的本地检查
debug调试辅助,提供诊断思路定位 bug 根因、分析错误堆栈
simplify代码简化与重构审查消除重复代码、降低圈复杂度
skillify将 prompt 转换为可复用的技能将一次性的 prompt 模板化
remember记忆管理(CLAUDE.md 条目)添加项目规范、团队约定
batch批量文件处理批量重命名、批量格式化
stuck帮助模型走出困境模型陷入循环时的”解围”指令
lorem-ipsum生成占位内容UI 开发时的占位文本
graph TD
    subgraph 代码质量层
        V["verify"]
        S["simplify"]
        D["debug"]
    end
    subgraph 工作流层
        B["batch"]
        ST["stuck"]
        SK["skillify"]
    end
    subgraph 配置层
        UC["update-config"]
        R["remember"]
        KH["keybindings-help"]
    end
    subgraph FG["Feature-gated 层(需特定功能标志)"]
        L["loop<br/>(TRIGGERS)"]
        CA["claude-api<br/>(APPS)"]
        SC["schedule<br/>(REMOTE)"]
    end

    style 代码质量层 fill:#e3f2fd,stroke:#1976D2,color:#1565C0
    style 工作流层 fill:#e8f5e9,stroke:#388E3C,color:#2E7D32
    style 配置层 fill:#fff3e0,stroke:#F57C00,color:#E65100
    style FG fill:#fce4ec,stroke:#C62828,color:#B71C1C

11.1.2 内置技能注册机制

每个内置技能通过注册函数注册,接收一个技能定义对象,包含以下关键字段:名称、描述、别名、使用场景提示、参数提示、允许的工具列表、模型指定、是否禁用模型调用、是否用户可调用、运行时门控回调、生命周期钩子、执行上下文(inline 或 fork)、关联的智能体、引用文件集合,以及最关键的 prompt 生成器函数。

其中最关键的几个字段:

  • prompt 生成器:延迟执行的 prompt 生成器,仅在技能被调用时才运行
  • files:可选的引用文件集合,首次调用时提取到磁盘,允许模型通过 Read/Grep 访问
  • isEnabled:运行时门控回调,如 /loop 技能每次调用时检查相关功能是否启用
  • context: 'fork':标记技能在独立子进程中执行
文件提取的安全设计

文件提取采用了精巧的惰性单例模式,使用 ??= 操作符确保并发调用共享同一个提取 Promise,避免竞态条件。文件写入使用 O_NOFOLLOW | O_EXCL 标志防止符号链接攻击,目录权限设置为 0o700,文件权限设置为 0o600

这些安全措施的含义:

安全措施保护的攻击向量工作原理
O_NOFOLLOW符号链接劫持如果路径是符号链接则打开失败
O_EXCLTOCTOU 竞态文件必须不存在,确保是新建而非覆盖
0o700 目录目录遍历只有所有者可以访问目录
0o600 文件文件泄露只有所有者可以读写文件
??= 单例竞态条件并发调用共享同一个 Promise

为什么技能需要引用文件?

某些技能(如 verify)在执行时需要参考额外的文档或配置。这些文件被编译进二进制文件中(不作为独立的磁盘文件存在),但在技能首次被调用时需要提取到磁盘。这样设计有两个好处:第一,技能的分发不需要额外的文件管理;第二,文件在提取时有完整的安全检查。

可以把它想象成”软件附带的说明书”——不需要时它不占地方(嵌入在二进制中),需要时才展开到桌面上(提取到临时目录)。

11.1.3 技能加载引擎

所有文件系统上的技能(非内置)由统一的加载引擎加载。这是一个会话内缓存的异步函数。

加载引擎的核心流程如下:

flowchart TD
    A["启动"] --> B["getSkillDirCommands(cwd)"]
    B --> C{"bare mode?"}
    C -->|"Yes"| C1["只加载 --add-dir 指定的路径"]
    C -->|"No"| D["并行加载五个来源"]
    D --> D1["[1] managedSkillsDir 管理策略级"]
    D --> D2["[2] userSkillsDir 用户级 ~/.claude/skills/"]
    D --> D3["[3] projectSkillsDirs 项目级 .claude/skills/"]
    D --> D4["[4] additionalDirs --add-dir 额外目录"]
    D --> D5["[5] legacyCommands 旧版 /commands/ 目录"]
    D1 --> E
    D2 --> E
    D3 --> E
    D4 --> E
    D5 --> E
    E["合并 + 按文件路径去重 (realpath)"] --> F["分离条件技能 (paths frontmatter) 和无条件技能"]
    F --> G["返回无条件技能列表"]

    style A fill:#3498db,stroke:#2471a3,color:#fff
    style G fill:#2ecc71,stroke:#27ae60,color:#fff

去重策略使用 realpath 解析符号链接得到规范路径,然后按首次出现的来源保留。这避免了通过符号链接或重叠的父目录导致的重复加载。

为什么需要去重?

考虑这样的目录结构:

/home/user/projects/my-app/        ← 项目目录
/home/user/projects/my-app/symlink ← 指向 .claude/skills/ 的符号链接

如果不做 realpath 解析,同一个技能目录可能通过两条路径被加载两次:一次通过原始路径 .claude/skills/my-skill/,一次通过符号链接 symlink/my-skill/。这会导致技能被注册两次,可能产生冲突或重复执行。realpath 解析确保无论通过什么路径访问,最终都指向同一个物理位置。


11.2 技能定义格式

11.2.1 Markdown Frontmatter 配置

文件型技能以目录形式组织,每个技能是一个名为 skill-name/ 的目录,其中包含一个 SKILL.md 文件。这是唯一的目录结构格式。

SKILL.md 支持 YAML frontmatter 配置。解析逻辑支持的字段包括:

---
name: my-skill-display-name
description: 这是一个示例技能
when_to_use: 当用户需要执行某操作时使用
arguments: arg1 arg2 arg3
argument-hint: "[arg1] [arg2] [arg3]"
allowed-tools:
  - Bash
  - Read
  - Write
model: claude-sonnet-4-20250514
effort: high
user-invocable: true
disable-model-invocation: false
context: fork
agent: code-builder
version: "1.0"
paths:
  - "src/**/*.ts"
  - "lib/**/*.js"
hooks:
  pre:
    - command: npm run lint
  post:
    - command: echo "done"
shell: bash
---

## 技能正文

这是技能的主要内容,支持 Markdown 格式。
可以使用 $ARGUMENTS 引用用户输入的参数。
使用 $1, $2 引用位置参数。
使用 ${CLAUDE_SKILL_DIR} 引用技能目录。
使用 ${CLAUDE_SESSION_ID} 引用当前会话 ID。
字段详解与最佳实践
字段类型必填说明最佳实践
namestring技能显示名称省略时使用目录名,建议使用 kebab-case
descriptionstring技能描述简洁明了,一句话说清功能
when_to_usestring使用场景描述帮助模型自动选择合适的技能
argumentsstring参数名列表用空格分隔,命名参数增强可读性
allowed-toolslist允许的工具遵循最小权限原则,只列出必要工具
modelstring使用的模型只读任务用 haiku,复杂任务用 inherit
contextstring执行上下文fork 用于耗时任务,inline 用于快速操作
pathslist条件激活路径用于按文件类型自动激活技能
hooksobject生命周期钩子pre/post 命令用于自动化检查

一个完整的技能创建流程示例:

~/.claude/skills/
  my-code-review/
    SKILL.md              ← 技能定义文件(必须)
    review-checklist.md   ← 附属文件,模型可按需读取
    examples/
      good-pattern.md     ← 良好模式示例
      bad-pattern.md      ← 反模式示例

11.2.2 参数替换机制

技能的 prompt 内容支持灵活的参数替换:

位置参数$ARGUMENTS 替换为完整参数字符串;$ARGUMENTS[0]$ARGUMENTS[1] 替换为索引参数;$0$1 是其简写形式。

命名参数:当 frontmatter 定义了 arguments: foo bar 时,$foo$bar 分别映射到对应位置的用户输入值。

替换逻辑按顺序执行:先替换命名参数,再替换索引参数,然后替换完整参数,最后若无占位符则追加参数文本。

参数替换执行顺序:

  输入:/my-skill hello world

  Step 1: 命名参数替换
    arguments: "greeting name"
    $greeting → "hello"
    $name → "world"

  Step 2: 索引参数替换
    $1 → "hello"($0 是技能名本身)
    $2 → "world"
    $ARGUMENTS[0] → "hello"
    $ARGUMENTS[1] → "world"

  Step 3: 完整参数替换
    $ARGUMENTS → "hello world"

  Step 4: 无占位符追加
    如果 prompt 中没有使用任何 $ 占位符
    → 将参数文本追加到 prompt 末尾

此外,技能正文还支持 Shell 命令内联执行和环境变量替换。在 prompt 生成器中,除了参数替换,还会:

  1. 添加技能根目录前缀:Base directory for this skill: ${baseDir}
  2. 替换 ${CLAUDE_SKILL_DIR} 为技能目录的绝对路径
  3. 替换 ${CLAUDE_SESSION_ID} 为当前会话 ID
  4. 对非 MCP 技能执行 Shell 命令内联展开(安全考虑:MCP 技能来自远端,不执行其内嵌 Shell 命令)

安全边界:为什么 MCP 技能不能执行 Shell 命令?

MCP 技能来自外部服务器,而 Shell 命令可以执行任意系统操作。如果允许 MCP 技能中的 Shell 命令内联展开,一个恶意的 MCP 服务器可以在技能 prompt 中注入 $(rm -rf /) 这样的命令。因此,系统在 LoadedFrom 类型为 mcp 时跳过 Shell 命令展开,这是一个关键的安全边界。

这是一个经典的”信任边界”问题——本地技能文件由用户创建和管理(可信),而 MCP 技能来自网络(不可信)。安全策略应该根据来源的可信程度而有所不同。

11.2.3 LoadedFrom 类型

技能的来源通过 LoadedFrom 类型标识,包含六种来源:旧版命令目录(commands_DEPRECATED)、新版技能目录(skills)、插件提供(plugin)、企业管理策略(managed)、内置编译(bundled)和 MCP 服务器提供(mcp)。

这个类型不仅用于追踪来源,还直接影响安全策略。例如,来自 MCP 的技能不会执行 Shell 命令内联,因为 MCP 技能来自不可信的远端服务器。

graph LR
    subgraph 来源["LoadedFrom 类型与安全策略映射"]
        direction TB
        B["bundled — 信任等级:最高<br/>Shell 内联:允许 / 文件提取:安全提取"]
        M["managed — 信任等级:高<br/>Shell 内联:允许 / 文件提取:安全提取"]
        S["skills — 信任等级:中<br/>Shell 内联:允许 / 文件提取:安全提取"]
        P["plugin — 信任等级:中<br/>Shell 内联:允许 / 文件提取:安全提取"]
        CD["commands_DEPRECATED — 信任等级:中<br/>Shell 内联:允许 / 文件提取:安全提取"]
        MCP["mcp — 信任等级:低<br/>Shell 内联:禁止 / 文件提取:N/A"]
    end

    B --> M --> S --> P --> CD --> MCP

    NOTE["安全策略根据来源信任等级递减"]

    style B fill:#2ecc71,stroke:#27ae60,color:#fff
    style M fill:#27ae60,stroke:#1e8449,color:#fff
    style S fill:#f39c12,stroke:#d68910,color:#fff
    style P fill:#f39c12,stroke:#d68910,color:#fff
    style CD fill:#e67e22,stroke:#d35400,color:#fff
    style MCP fill:#e74c3c,stroke:#c0392b,color:#fff
    style NOTE fill:#fff3e0,stroke:#f39c12,color:#333

11.3 技能加载路径

11.3.1 分层加载策略

getSkillsPath 函数清晰地定义了各来源的路径映射,根据来源类型(policySettings、userSettings、projectSettings、plugin)返回对应的技能目录路径。

各层级说明:

管理策略级(Managed / Policy):由企业管理员通过集中配置下发。路径为 {managedPath}/.claude/skills/。可通过 CLAUDE_CODE_DISABLE_POLICY_SKILLS 环境变量禁用。

用户级(User):存储在用户主目录 ~/.claude/skills/。跨所有项目可用。需要 userSettings 来源启用且不受 pluginOnly 策略限制。

项目级(Project):存储在项目目录 .claude/skills/。仅在该项目上下文中生效。支持嵌套目录发现(见下节)。

额外目录(Additional):通过 --add-dir CLI 参数指定的额外目录中的 .claude/skills/ 子目录。

旧版命令目录(Legacy Commands).claude/commands/ 目录,兼容旧格式。单文件 .md 和目录格式 SKILL.md 均支持。该来源标记为 commands_DEPRECATED

graph TD
    A["管理策略级 (Managed)<br/>路径: {managedPath}/.claude/skills/<br/>企业标准,最高优先级"]
    B["项目级 (Project)<br/>路径: .claude/skills/<br/>项目特定技能"]
    C["用户级 (User)<br/>路径: ~/.claude/skills/<br/>个人偏好技能"]
    D["插件级 (Plugin)<br/>路径: {plugin-dir}/commands/<br/>插件提供的技能"]
    E["内置级 (Bundled)<br/>编译进二进制文件<br/>开箱即用的核心技能"]

    A -->|"覆盖"| B
    B -->|"覆盖"| C
    C -->|"覆盖"| D
    D -->|"覆盖"| E

    style A fill:#e74c3c,stroke:#c0392b,color:#fff
    style B fill:#f39c12,stroke:#d68910,color:#fff
    style C fill:#3498db,stroke:#2471a3,color:#fff
    style D fill:#9b59b6,stroke:#7d3c98,color:#fff
    style E fill:#bdc3c7,stroke:#7f8c8d,color:#333

11.3.2 动态技能发现

除了启动时的静态加载,Claude Code 还支持在会话运行时动态发现技能。当模型通过 Read、Write、Edit 等工具访问文件时,系统会沿文件路径向上查找 .claude/skills/ 目录。

核心函数 discoverSkillDirsForPaths 实现了动态技能发现机制:

flowchart TD
    A["文件操作触发: Read(src/sub/deep/file.ts)"]
    B["从 src/sub/deep/ 开始向上遍历至 cwd"]
    C["检查每层是否存在 .claude/skills/ 目录"]
    D["跳过 gitignored 目录"]
    E["收集新发现的目录(按深度排序,最深的优先)"]
    F["addSkillDirectories() 加载并合并到 dynamicSkills Map"]

    A --> B --> C --> D --> E --> F

    style A fill:#3498db,stroke:#2471a3,color:#fff
    style F fill:#2ecc71,stroke:#27ae60,color:#fff

关键的安全设计:被 .gitignore 忽略的目录中的技能会被自动跳过,防止 node_modules/pkg/.claude/skills/ 这类路径被静默加载。

反模式警告:不要在 node_modules 中放置技能

虽然 .gitignore 过滤提供了安全防线,但依赖它作为唯一的安全措施是不够的。如果一个 npm 包在 .claude/skills/ 中放置了技能文件,而该目录碰巧不在 .gitignore 中,这些技能会被自动加载。建议在项目中明确将 **/.claude/skills/ 添加到 .gitignore,只保留根目录的 .claude/skills/

实际使用场景:Monorepo 中的项目特定技能

在 Monorepo 结构中,每个子项目可能有自己的技能需求。动态技能发现机制完美地解决了这个问题:

my-monorepo/
├── packages/
│   ├── frontend/
│   │   └── .claude/skills/
│   │       └── component-gen/
│   │           └── SKILL.md    ← 前端组件生成技能
│   ├── backend/
│   │   └── .claude/skills/
│   │       └── api-gen/
│   │           └── SKILL.md    ← API 端点生成技能
│   └── shared/
│       └── .claude/skills/
│           └── type-gen/
│               └── SKILL.md    ← 共享类型生成技能

当模型操作 packages/frontend/ 下的文件时,会自动发现前端的组件生成技能;操作 packages/backend/ 下的文件时,会自动发现 API 生成技能。这种”按位置激活”的模式极大地减少了技能选择的心智负担。

11.3.3 条件技能

带有 paths frontmatter 的技能不会立即激活,而是存入 conditionalSkills Map。当模型操作的文件路径与 paths 模式匹配时,该技能才会被激活。

匹配使用 gitignore 风格的模式,通过 ignore 库实现。当文件路径与条件技能的 paths 模式匹配时,该技能会被激活并存入动态技能集合。已激活的条件技能名称会被永久记录,即使后续清空缓存也不会重新变为条件状态。

flowchart TD
    A["启动时加载技能列表"] --> B{"有 paths 字段?"}
    B -->|"No"| C["立即可用"]
    B -->|"Yes"| D["存入 conditionalSkills Map"]
    D --> E["模型操作文件<br/>Read/Write/Edit"]
    E --> F{"文件路径匹配 paths 模式?"}
    F -->|"No"| G["不激活"]
    F -->|"Yes"| H["激活技能<br/>存入 dynamicSkills + 永久标记已激活"]

为什么已激活的条件技能需要永久标记?

假设你定义了一个条件技能 paths: ["src/**/*.ts"],当模型第一次读取 src/foo.ts 时被激活。如果后来因为缓存清理导致它被移回条件状态,那么除非模型再次读取匹配的文件,否则这个技能就”消失”了。但此时模型可能正在基于这个技能的指导执行任务,技能的突然消失会导致行为不一致。永久标记确保一旦激活,技能在整个会话期间都保持可用。


11.4 插件系统

11.4.1 插件目录结构

插件是比技能更高层级的扩展单元。一个插件可以包含技能、agents、hooks 等多种组件。其目录结构通常包含:

my-plugin/
├── plugin.json           ← 插件清单(可选)
├── commands/             ← 自定义 slash 命令
│   ├── build.md
│   └── deploy.md
├── agents/               ← 自定义 AI agents
│   └── test-runner.md
└── hooks/
    └── hooks.json        ← Hook 配置

插件清单 plugin.json 包含插件的元信息:

字段说明用途
name插件名称显示和引用
version版本号兼容性检查
description描述文本用户识别
commands命令目录路径发现自定义命令
agents智能体目录路径发现自定义智能体
hooksHook 配置路径生命周期钩子

插件的发现来源按优先级排列:

  1. Marketplace 插件:通过 plugin@marketplace 格式在 settings 中配置
  2. Session-only 插件:通过 --plugin-dir CLI 参数或 SDK plugins 选项加载

交叉引用: 插件提供的智能体(agents/)使用与第9章中自定义智能体相同的 Markdown 格式。区别在于来源——自定义智能体来自 .claude/agents/ 目录,插件智能体来自插件的 agents/ 目录,两者的优先级不同。

11.4.2 缓存优先加载策略

插件加载采用缓存优先策略以避免重复的文件系统 I/O。缓存优先函数优先从缓存返回已加载的插件数据,仅在缓存为空时才执行实际的磁盘读取。

整个加载流程:

flowchart TD
    A["收集所有插件来源<br/>marketplace / session-only / builtin"] --> B["对每个插件目录:<br/>检查缓存"]
    B -->|"命中"| C["直接返回"]
    B -->|"未命中"| D["解析 plugin.json 清单"]
    D --> E["加载 commands/ 目录中的技能"]
    D --> F["加载 agents/ 目录中的智能体"]
    D --> G["验证 hooks 配置并 解析变量"]
    D --> H["收集错误(不中断加载)"]
    I["存入缓存"]
    I --> J["返回 PluginLoadResult<br/>plugins: Plugin[]<br/>errors: LoadError[]"]

    style A fill:#3498db,stroke:#2471a3,color:#fff
    style C fill:#f39c12,stroke:#d68910,color:#fff
    style D fill:#2ecc71,stroke:#27ae60,color:#fff
    style I fill:#1abc9c,stroke:#16a085,color:#fff
    style J fill:#e67e22,stroke:#d35400,color:#fff

设计洞察:为什么”收集错误而不中断”?

插件加载采用”尽力而为”策略——即使某个插件的一个组件加载失败(比如一个命令的 Markdown 格式错误),其他组件仍然可以正常加载。这避免了”一个坏苹果毁了整桶”的问题。错误被收集到 PluginLoadResult.errors 中,可以在后续进行诊断和修复。

这种容错设计在插件系统中非常重要。想象一下,如果因为一个第三方插件的格式错误导致整个 Claude Code 无法启动,用户体验将是灾难性的。

11.4.3 MCP 技能的间接注册

MCP 服务器也可以提供技能。为了避免循环依赖,MCP 技能的构建函数通过一个中间注册模块间接获取。技能加载模块初始化时注册构建函数,MCP 技能模块需要时再获取。这是一个经典的”发布-查找”(publish-lookup)模式,在依赖图中插入一个叶子节点来打破循环。不使用动态 import 的原因是:在打包的二进制文件中,非字面量的动态 import 会因路径解析失败而报错。

依赖关系与循环打破:

  正常依赖链(会产生循环):
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  skills   │────▶│   mcp    │────▶│  skills   │  ← 循环!
  │  module   │◀────│  module  │◀────│  module   │
  └──────────┘     └──────────┘     └──────────┘

  使用中间注册模块打破循环:
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  skills   │────▶│ registry │◀────│   mcp    │
  │  module   │     │  module  │     │  module   │
  └──────────┘     └──────────┘     └──────────┘
       │                                  │
       │  publish(buildFn)                │  lookup(buildFn)
       └──────────┐         ┌────────────┘
                  ▼         ▼
            ┌──────────────────┐
            │  中间注册模块      │  ← 依赖图的叶子节点
            │  (无自身依赖)      │
            └──────────────────┘

为什么不使用动态 import?

Claude Code 的二进制文件是通过打包工具(如 esbuild)将所有 TypeScript 模块编译合并而成的。动态 import 使用字符串变量作为路径时,打包工具无法在编译时确定要包含哪些模块,导致路径解析失败。“发布-查找”模式通过在编译时就确定所有依赖关系,避免了运行时的动态解析问题。


11.5 创建自定义技能完整教程

本节通过一个完整的案例,展示从零开始创建一个实用技能的全过程。

案例:创建一个 API 端点生成技能

目标: 创建一个技能,当用户输入 /gen-api GET users 时,自动生成完整的 REST API 端点代码,包括路由、控制器、类型定义和测试。

Step 1: 创建技能目录
mkdir -p ~/.claude/skills/gen-api
Step 2: 编写 SKILL.md
---
name: gen-api
description: 生成 REST API 端点的完整代码骨架
when_to_use: 当用户需要创建新的 API 端点或 CRUD 操作时
arguments: method resource
argument-hint: "[GET|POST|PUT|DELETE] [resource-name]"
allowed-tools:
  - Read
  - Write
  - Grep
  - Glob
model: inherit
effort: high
user-invocable: true
---

## API 端点生成器

为资源 $resource 创建 $method 方法的 REST API 实现。

### 上下文收集

首先执行以下调查步骤:

1. 使用 Grep 查找现有的路由定义模式
2. 使用 Read 查看一个已有的路由文件作为参考
3. 使用 Glob 查找类型定义的位置
4. 使用 Grep 查找现有的测试文件结构

### 生成规范

基于调查结果,生成以下文件:

#### 1. 路由定义
- 文件位置:参考现有路由文件的模式
- 包含请求验证中间件
- 包含认证/授权检查

#### 2. 类型定义
- Request body 类型
- Response 类型
- Query 参数类型

#### 3. 控制器逻辑
- 输入验证
- 业务逻辑
- 错误处理
- 响应格式化

#### 4. 测试文件
- 正常路径测试
- 边界条件测试
- 错误处理测试

### 注意事项

- 遵循项目中已有的代码风格和模式
- 使用项目中已有的错误处理工具
- 如果项目使用 ORM,使用对应的查询语法
Step 3: 测试技能

启动 Claude Code 后输入 /gen-api GET users,验证:

  • 技能是否被正确识别和加载
  • 参数替换是否正确($method = “GET”,$resource = “users”)
  • 生成的代码是否符合项目既有模式
Step 4: 迭代优化

根据使用反馈持续优化:

  • 如果发现模型经常遗漏某个步骤,在 prompt 中增加更强的指令
  • 如果生成的代码风格不一致,在 prompt 中加入更具体的风格要求
  • 如果需要支持更多参数,扩展 arguments 字段

技能组合使用的最佳实践

技能不是孤立使用的——它们可以与其他技能、智能体和工具组合形成强大的工作流。

模式一:技能链
技能链示例:API 开发工作流

  /gen-api POST orders     → 生成 API 骨架


  /verify                   → 验证生成的代码


  /remember "使用 RESTful 路由命名"  → 记录项目约定
模式二:条件技能 + 智能体
条件技能 + 智能体组合:

  1. 定义条件技能(paths: ["src/api/**/*.ts"])
     → 只在 API 文件上激活

  2. 自定义智能体引用该技能
     → 智能体在处理 API 任务时自动使用技能

  3. 技能中引用附属文件
     → 技能目录下的模式文件作为参考
模式三:技能 + Hook
## SKILL.md
---
hooks:
  pre:
    - command: npm run type-check
  post:
    - command: npm run test -- --related
---

## 技能内容
生成代码前先运行类型检查(pre hook),
完成后运行相关测试(post hook)。

反模式警告:不要创建”万能技能”

一个技能应该做一件事并做好。如果你发现一个技能的 prompt 超过了 200 行,考虑将它拆分为多个技能。例如,不要创建一个”全栈开发”技能,而是创建”API 生成”、“前端组件生成”、“测试生成”三个独立的技能。

原因:技能越大,模型执行时的不确定性越高。小而专注的技能更容易调试、更容易复用、更容易与其他技能组合。


实战练习

练习 1:创建一个自定义项目级技能

在项目根目录创建 .claude/skills/code-review/SKILL.md

---
description: 执行代码审查,关注安全性、性能和可维护性
arguments: file_path
allowed-tools:
  - Read
  - Grep
  - Glob
paths:
  - "src/**/*.ts"
  - "src/**/*.tsx"
---

对文件 $file_path 进行全面代码审查。

检查以下方面:
1. 安全漏洞(XSS、注入、敏感数据泄露)
2. 性能问题(不必要的循环、内存泄漏风险)
3. 可维护性(命名、复杂度、重复代码)
4. 错误处理的完整性

输出结构化的审查报告。

扩展挑战: 在技能目录下创建一个 review-checklist.md 附属文件,包含详细的审查检查清单,让技能 prompt 引用这个文件。

练习 2:创建一个带命名参数的用户级技能

~/.claude/skills/generate-api/SKILL.md 创建技能:

---
description: 生成 REST API 端点的样板代码
arguments: method resource_name
allowed-tools:
  - Write
  - Read
---

为资源 '$resource_name' 创建 $method 方法的 REST API 实现。

生成以下文件:
1. 路由定义
2. 请求/响应类型
3. 控制器逻辑
4. 基本测试

使用:/generate-api POST user

思考题: 如果用户输入 /generate-api 而不带任何参数会发生什么?如何改进技能定义来处理参数缺失的情况?

练习 3:探索技能加载的调试日志

运行 CLAUDE_CODE_DEBUG=1 claude 启动会话,观察技能加载的调试输出,了解当前环境中加载了哪些来源的技能。

检查清单:

  • 确认内置技能是否全部加载
  • 检查是否有旧版命令目录的兼容加载
  • 查看条件技能的激活情况
  • 验证去重策略是否正常工作

练习 4:创建一个带生命周期钩子的技能

创建一个技能,在执行前自动运行 lint 检查,在执行后自动运行相关测试。思考:

  • pre hook 失败时应该如何处理?
  • post hook 的输出如何影响技能结果?
  • 如何在 hook 中引用技能的参数?

练习 5:设计一个条件技能系统

为一个 React + TypeScript 项目设计以下条件技能:

  1. 当操作 src/components/ 下的文件时,激活”组件生成”技能
  2. 当操作 src/hooks/ 下的文件时,激活”Hook 生成”技能
  3. 当操作 src/api/ 下的文件时,激活”API 集成”技能
  4. 当操作 *.test.* 文件时,激活”测试辅助”技能

思考:这些技能之间是否有重叠?如何避免技能之间的冲突?


关键要点

  1. 分层架构:技能来源从 managed -> user -> project -> plugin -> bundled 逐层覆盖,企业策略具有最高优先权。每一层都有独立的路径和安全策略。

  2. 安全优先:MCP 技能(loadedFrom === 'mcp')不执行 Shell 命令内联;内置技能的文件提取使用 O_NOFOLLOW | O_EXCL 防止符号链接攻击;gitignored 目录中的技能被自动跳过。安全策略根据来源信任等级递减。

  3. 惰性加载:内置技能的引用文件在首次调用时才提取到磁盘;条件技能在匹配文件模式时才激活;动态技能在文件操作触发路径扫描时才被发现。这种惰性策略优化了启动性能和内存使用。

  4. 去重策略:使用 realpath 解析符号链接得到规范路径,按首次出现保留,避免重复加载。这解决了符号链接和重叠父目录导致的问题。

  5. 循环依赖解决:通过中间注册模块的”发布-查找”模式,在依赖图中插入叶子节点打破循环。选择这种模式而非动态 import 是因为二进制打包的限制。

  6. 参数系统的灵活性:支持 $ARGUMENTS(完整参数)、$ARGUMENTS[N]/$N(位置参数)、命名参数($foo)三级替换,加上 ${CLAUDE_SKILL_DIR}${CLAUDE_SESSION_ID} 环境变量。替换顺序确保了参数名不会与位置索引冲突。

  7. 插件作为扩展单元:插件比技能更高层级,可以包含技能、智能体和钩子。缓存优先加载和容错错误收集确保了插件的可靠性和性能。

  8. 技能组合模式:技能可以与其他技能、智能体和 Hook 组合使用。保持技能小而专注,通过组合实现复杂工作流。