《Claude Code 设计与实现》完整目录
- 前言
- 第1章 为什么需要理解 Claude Code
- 第2章 架构总览
- 第3章 CLI 启动与性能优化
- 第4章 Query 引擎:Agent 的心脏
- 第5章 流式消息与状态机
- 第6章 工具类型系统设计
- 第7章 工具编排与并发执行
- 第8章 核心工具实现剖析
- 第9章 多模式权限模型
- 第10章 Bash 安全与沙箱
- 第11章 MCP 协议集成
- 第12章 IDE Bridge 通信架构
- 第13章 LSP 与语言服务
- 第14章 多 Agent 协调与 Swarm
- 第15章 Skill 与插件系统(当前)
- 第16章 上下文管理与自动压缩
- 第17章 React + Ink 终端 UI
- 第18章 设计模式与架构决策
第15章 Skill 与插件系统
开篇引言
在前面的章节中,我们深入分析了 Claude Code 的工具系统、权限模型和多 Agent 协作架构。这些机制构成了系统的核心骨骼,但真正赋予 Claude Code 生命力的,是其高度可扩展的 Skill 与插件系统。
想象一个开发者的日常场景:团队内部有一套定制化的代码审查规范,希望 Claude Code 在每次代码变更后自动执行检查;或者需要为特定框架编写一套部署流程,让模型在用户说出 /deploy 时自动执行一系列预定义操作。传统的工具系统虽然强大,但每添加一种新能力都需要修改核心代码。这就引出了一个根本性的架构问题:如何在不修改系统核心的前提下,让用户和社区自由扩展 Claude Code 的能力?
Claude Code 的回答是构建了三层递进的扩展体系:Skill(技能) 提供了声明式的能力描述机制,通过 Markdown 文件即可定义新技能;Plugin(插件) 在 Skill 之上增加了组件化封装,支持技能、Hooks、MCP 服务器的捆绑分发;Hooks(钩子) 则深入到工具执行的生命周期中,允许在关键节点插入自定义逻辑。三者协同工作,配合统一的 Slash 命令系统,形成了一个既灵活又安全的扩展架构。
本章将从源码层面深入剖析这套扩展体系的设计与实现,揭示其背后的架构决策和工程智慧。
本章要点
- Skill 系统的三种来源 :文件系统 Skill(
.claude/skills/目录下的 Markdown 文件)、Bundled Skill(编译到 CLI 二进制中的内置技能)、MCP Skill(通过 MCP 协议远程加载的技能),以及它们如何统一转换为Command对象 - BundledSkillDefinition 类型:内置技能的声明式定义,包括名称、描述、触发条件、允许的工具列表、执行上下文等关键字段的设计考量
- SkillTool 的完整生命周期:从输入验证、权限检查、命令查找,到 inline/fork 两种执行模式的分流机制
- Plugin 系统的分层架构 :BuiltinPlugin(内置插件)与 Marketplace Plugin(市场插件)的双轨机制,以及
LoadedPlugin类型如何统一表示两者 - Hooks 的四种类型:command(Shell 命令)、prompt(LLM 提示)、agent(Agent 验证器)、http(HTTP 回调),以及它们在 PreToolUse/PostToolUse 等事件节点上的精密执行逻辑
- Slash 命令系统的统一注册架构:80+ 命令的分类管理、优先级机制和动态加载策略
- 三个子系统的协作关系:Skill 定义能力,Plugin 封装和分发能力,Hooks 在执行时拦截和增强能力
15.1 Skill 系统
Claude Code 的扩展体系由 Skill、Plugin 和 Hooks 三层构成,它们通过统一的 Slash 命令系统对外暴露。下图展示了三个子系统之间的关系:
15.1.1 Skill 的本质:声明式能力描述
在 Claude Code 的架构中,Skill 本质上是一种声明式的能力描述 。每个 Skill 最终都被转换为一个 Command 对象,其中最重要的是 getPromptForCommand 方法------它返回一段 prompt 内容,指导模型如何完成特定任务。这种设计意味着,添加新能力不需要编写任何 TypeScript 代码,只需创建一个 Markdown 文件并用 frontmatter 声明元数据即可。
Command 类型定义在 src/types/command.ts 中,是一个联合类型:
typescript
// 文件: src/types/command.ts
export type PromptCommand = {
type: 'prompt'
progressMessage: string
contentLength: number
argNames?: string[]
allowedTools?: string[]
model?: string
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
hooks?: HooksSettings
skillRoot?: string
context?: 'inline' | 'fork'
agent?: string
effort?: EffortValue
paths?: string[]
getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)
这里的 source 字段清晰地标记了命令的来源:builtin 表示硬编码的内部命令(如 /help、/clear),bundled 表示编译进二进制的内置 Skill,plugin 表示来自插件,mcp 表示来自 MCP 服务器。这个字段在后续的权限检查、遥测上报、prompt 截断等环节都起到了关键的分流作用。
15.1.2 skills/ 目录结构
Skill 系统的源码组织在 src/skills/ 目录下:
lua
src/skills/
bundledSkills.ts -- Bundled Skill 注册表与类型定义
loadSkillsDir.ts -- 文件系统 Skill 加载器
mcpSkillBuilders.ts -- MCP Skill 构建器注册表
bundled/
index.ts -- 所有 Bundled Skill 的初始化入口
simplify.ts -- /simplify 代码审查技能
updateConfig.ts -- /update-config 配置管理技能
keybindings.ts -- /keybindings 快捷键管理技能
verify.ts -- /verify 验证技能
claudeApi.ts -- /claude-api API 开发技能
batch.ts -- /batch 批量处理技能
loop.ts -- /loop 循环执行技能
remember.ts -- /remember 记忆技能
stuck.ts -- /stuck 卡住恢复技能
debug.ts -- /debug 调试技能
... 更多内置技能
这个目录结构体现了清晰的职责分离:bundledSkills.ts 负责类型定义和注册机制,loadSkillsDir.ts 负责从磁盘加载用户自定义 Skill,bundled/ 目录存放所有编译到二进制中的内置 Skill 实现。
15.1.3 BundledSkillDefinition 类型
BundledSkillDefinition 是定义内置 Skill 的核心类型,位于 src/skills/bundledSkills.ts:
typescript
// 文件: src/skills/bundledSkills.ts
export type BundledSkillDefinition = {
name: string
description: string
aliases?: string[]
whenToUse?: string
argumentHint?: string
allowedTools?: string[]
model?: string
disableModelInvocation?: boolean
userInvocable?: boolean
isEnabled?: () => boolean
hooks?: HooksSettings
context?: 'inline' | 'fork'
agent?: string
files?: Record<string, string>
getPromptForCommand: (
args: string,
context: ToolUseContext,
) => Promise<ContentBlockParam[]>
}
每个字段都承载着精确的设计意图:
whenToUse:详细描述 Skill 的适用场景,在 SkillTool 的 prompt 中呈现给模型,帮助模型判断何时应自动调用该 SkillallowedTools:声明该 Skill 执行时需要的工具白名单,SkillTool 会通过contextModifier临时将这些工具添加到权限系统中context:'inline'表示 Skill 内容直接展开到当前对话中,'fork'表示在独立的 sub-agent 中执行,隔离上下文和 token 预算files:附加的参考文件,在首次调用时惰性提取到磁盘,模型可以通过 Read/Grep 工具按需访问disableModelInvocation:设为true时,模型无法通过 SkillTool 自动调用该 Skill,仅允许用户通过/前缀手动触发
files 字段的设计尤其值得关注。由于 Bundled Skill 编译进二进制文件中,没有磁盘上的文件目录可供模型读取。通过 files 字段,Skill 可以声明一组参考文件,系统会在首次调用时将它们提取到 getBundledSkillsRoot() 下的安全目录中:
typescript
// 文件: src/skills/bundledSkills.ts
if (files && Object.keys(files).length > 0) {
skillRoot = getBundledSkillExtractDir(definition.name)
let extractionPromise: Promise<string | null> | undefined
const inner = definition.getPromptForCommand
getPromptForCommand = async (args, ctx) => {
// 闭包级备忘录:每个进程只提取一次
// 对 Promise 做备忘,而非对结果做备忘,
// 使得并发调用者等待同一次提取,而不是竞争写入
extractionPromise ??= extractBundledSkillFiles(definition.name, files)
const extractedDir = await extractionPromise
const blocks = await inner(args, ctx)
if (extractedDir === null) return blocks
return prependBaseDir(blocks, extractedDir)
}
}
这里用 ??= 运算符做惰性初始化,并且是对 Promise 本身做缓存而非对结果做缓存,这样即使多个并发请求同时到达,也只会执行一次文件提取操作。
15.1.4 Skill 的注册与发现
Bundled Skill 的注册遵循一个简洁的模式:在 src/skills/bundled/index.ts 的 initBundledSkills() 函数中依次调用各个 Skill 的注册函数:
typescript
// 文件: src/skills/bundled/index.ts
export function initBundledSkills(): void {
registerUpdateConfigSkill()
registerKeybindingsSkill()
registerVerifySkill()
registerDebugSkill()
registerSimplifySkill()
registerBatchSkill()
registerStuckSkill()
// ... 更多 Skill 注册
// 部分 Skill 受特性开关控制
if (feature('BUILDING_CLAUDE_APPS')) {
const { registerClaudeApiSkill } = require('./claudeApi.js')
registerClaudeApiSkill()
}
}
每个注册函数内部调用 registerBundledSkill(),该函数将 BundledSkillDefinition 转换为 Command 对象并存入内部注册表:
typescript
// 文件: src/skills/bundledSkills.ts
const bundledSkills: Command[] = []
export function registerBundledSkill(definition: BundledSkillDefinition): void {
const command: Command = {
type: 'prompt',
name: definition.name,
description: definition.description,
allowedTools: definition.allowedTools ?? [],
source: 'bundled',
loadedFrom: 'bundled',
// ... 将 BundledSkillDefinition 字段映射到 Command 字段
getPromptForCommand,
}
bundledSkills.push(command)
}
Skill 的发现过程由 commands.ts 中的 getSkills() 函数统一协调,它并行加载四种来源的 Skill:
typescript
// 文件: src/commands.ts
async function getSkills(cwd: string): Promise<{
skillDirCommands: Command[]
pluginSkills: Command[]
bundledSkills: Command[]
builtinPluginSkills: Command[]
}> {
const [skillDirCommands, pluginSkills] = await Promise.all([
getSkillDirCommands(cwd), // 文件系统 Skill
getPluginSkills(), // 插件 Skill
])
const bundledSkills = getBundledSkills() // 内置 Skill
const builtinPluginSkills = getBuiltinPluginSkillCommands() // 内置插件 Skill
return { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills }
}
所有这些来源最终在 loadAllCommands() 中合并为一个统一的命令列表,合并时的顺序非常重要------Bundled Skill 优先于文件系统 Skill,文件系统 Skill 优先于插件 Skill,插件 Skill 优先于内置命令:
typescript
// 文件: src/commands.ts
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
const [
{ skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
pluginCommands,
workflowCommands,
] = await Promise.all([
getSkills(cwd),
getPluginCommands(),
getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
])
return [
...bundledSkills,
...builtinPluginSkills,
...skillDirCommands,
...workflowCommands,
...pluginCommands,
...pluginSkills,
...COMMANDS(), // 内置的 slash 命令
]
})
15.1.5 以 simplify 为例:一个 Bundled Skill 的完整实现
/simplify 是一个典型的 Bundled Skill,它演示了如何用最少的代码定义一个功能完整的代码审查技能:
typescript
// 文件: src/skills/bundled/simplify.ts
const SIMPLIFY_PROMPT = `# Simplify: Code Review and Cleanup
Review all changed files for reuse, quality, and efficiency. Fix any issues found.
## Phase 1: Identify Changes
Run \`git diff\` to see what changed.
## Phase 2: Launch Three Review Agents in Parallel
Use the ${AGENT_TOOL_NAME} tool to launch all three agents concurrently...
// ... 详细的审查指令
`
export function registerSimplifySkill(): void {
registerBundledSkill({
name: 'simplify',
description:
'Review changed code for reuse, quality, and efficiency, then fix any issues found.',
userInvocable: true,
async getPromptForCommand(args) {
let prompt = SIMPLIFY_PROMPT
if (args) {
prompt += `\n\n## Additional Focus\n\n${args}`
}
return [{ type: 'text', text: prompt }]
},
})
}
这里的核心思想是:Skill 不需要任何"代码逻辑",只需要一段精心编写的 prompt。模型读到这段 prompt 后,会自动使用 AgentTool 并行启动三个子 Agent,分别执行代码复用检查、代码质量审查和效率审查。整个编排逻辑全部交给模型自主决策。
15.1.6 文件系统 Skill 的加载机制
除了 Bundled Skill,用户还可以通过在 .claude/skills/ 目录中放置 Markdown 文件来定义自定义 Skill。loadSkillsDir.ts 中的加载器负责解析这些文件的 frontmatter 元数据:
typescript
// 文件: src/skills/loadSkillsDir.ts
export function parseSkillFrontmatterFields(
frontmatter: FrontmatterData,
markdownContent: string,
resolvedName: string,
): {
displayName: string | undefined
description: string
allowedTools: string[]
whenToUse: string | undefined
model: ReturnType<typeof parseUserSpecifiedModel> | undefined
disableModelInvocation: boolean
hooks: HooksSettings | undefined
executionContext: 'fork' | undefined
agent: string | undefined
effort: EffortValue | undefined
// ... 更多字段
}
一个自定义 Skill 的 Markdown 文件示例:
markdown
---
description: 执行项目部署流程
when_to_use: 当用户请求部署或发布时
allowed-tools: ["Bash"]
context: fork
---
# 部署流程
1. 运行测试确保所有用例通过
2. 构建生产版本
3. 推送到部署环境
createSkillCommand() 函数将解析后的元数据和 Markdown 内容组合成 Command 对象。其中 getPromptForCommand 方法会执行一系列替换操作------$ARGUMENTS 替换为用户传入的参数,${CLAUDE_SKILL_DIR} 替换为技能目录的绝对路径,${CLAUDE_SESSION_ID} 替换为当前会话 ID。
15.1.7 SkillTool 的实现
SkillTool 是 Skill 系统的运行时入口,是模型用来调用 Skill 的工具。它定义在 src/tools/SkillTool/SkillTool.ts 中,其工作流程如下:
scss
用户/模型调用 SkillTool
|
v
validateInput() -- 检查 Skill 名称是否有效、是否存在、是否允许模型调用
|
v
checkPermissions() -- 查找 deny/allow 规则,对安全 Skill 自动放行
|
v
call() -- 执行 Skill
/ \
/ \
inline fork
| |
展开到 在子 Agent
当前对话 中隔离执行
输入验证 阶段,SkillTool 接受两个参数------skill(技能名称)和可选的 args(参数)。验证逻辑确保:命令存在、不是 disableModelInvocation 命令、且为 prompt 类型命令。
权限检查 阶段的设计非常精巧。它引入了一个"安全属性白名单"的概念:
typescript
// 文件: src/tools/SkillTool/SkillTool.ts
const SAFE_SKILL_PROPERTIES = new Set([
'type', 'progressMessage', 'contentLength', 'model', 'effort',
'source', 'name', 'description', 'aliases', 'argumentHint',
'whenToUse', 'disableModelInvocation', 'userInvocable',
'loadedFrom', 'getPromptForCommand',
// ... 更多安全属性
])
function skillHasOnlySafeProperties(command: Command): boolean {
for (const key of Object.keys(command)) {
if (SAFE_SKILL_PROPERTIES.has(key)) continue
const value = (command as Record<string, unknown>)[key]
if (value === undefined || value === null) continue
if (Array.isArray(value) && value.length === 0) continue
return false
}
return true
}
这个设计的关键在于默认否认 :如果未来在 Command 类型上添加了新属性,新属性默认不在安全白名单中,因此带有新属性的 Skill 会自动要求用户确认权限。这避免了因遗忘更新白名单而引入安全漏洞。
执行 阶段根据 context 字段分为两种模式:
-
inline 模式 (默认):Skill 的 prompt 内容被包装为用户消息,注入到当前对话上下文中。模型在后续推理中会看到这些内容并据此行动。SkillTool 还通过
contextModifier机制临时修改上下文,添加工具权限和模型覆盖。 -
fork 模式 :Skill 在一个独立的 sub-agent 中执行。
executeForkedSkill()调用runAgent()启动子 Agent,子 Agent 拥有独立的消息历史和 token 预算。执行结果通过extractResultText()提取后作为工具结果返回给主对话。
typescript
// 文件: src/tools/SkillTool/SkillTool.ts(简化)
async call({ skill, args }, context, canUseTool, parentMessage, onProgress?) {
const commandName = skill.trim().replace(/^\//, '')
const commands = await getAllCommands(context)
const command = findCommand(commandName, commands)
// fork 模式:在子 Agent 中执行
if (command?.type === 'prompt' && command.context === 'fork') {
return executeForkedSkill(command, commandName, args, context, ...)
}
// inline 模式:展开到当前对话
const processedCommand = await processPromptSlashCommand(
commandName, args || '', commands, context,
)
return {
data: { success: true, commandName, allowedTools, model },
newMessages,
contextModifier(ctx) { /* 修改工具权限和模型 */ },
}
}
15.1.8 Skill 的 prompt 生成与预算管理
SkillTool 的 prompt 不仅包含工具使用说明,还包含所有可用 Skill 的列表。这个列表在 src/tools/SkillTool/prompt.ts 中生成,并受到严格的预算控制:
typescript
// 文件: src/tools/SkillTool/prompt.ts
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01 // 上下文窗口的 1%
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000 // 回退值:200K * 4 * 1%
export const MAX_LISTING_DESC_CHARS = 250 // 单条描述的硬上限
当 Skill 列表超出预算时,系统采用分级截断策略:Bundled Skill 的描述永远保留完整(因为它们是官方核心能力),非 Bundled Skill 的描述则按比例缩短。极端情况下,非 Bundled Skill 甚至会退化为仅显示名称。
15.2 插件系统
15.2.1 plugins/ 目录结构
插件系统是 Skill 系统之上的更高层抽象,它允许将多个 Skill、Hooks、MCP 服务器、LSP 服务器打包为一个可分发的单元。源码组织如下:
lua
src/plugins/
builtinPlugins.ts -- 内置插件注册表
bundled/
index.ts -- 内置插件初始化入口
src/types/
plugin.ts -- 核心类型定义(BuiltinPluginDefinition, LoadedPlugin, PluginError 等)
src/utils/plugins/
loadPluginCommands.ts -- 插件命令/技能加载器
pluginLoader.ts -- 插件加载核心逻辑
pluginIdentifier.ts -- 插件标识符解析
pluginOptionsStorage.ts -- 插件配置持久化
pluginDirectories.ts -- 插件目录管理
cacheUtils.ts -- 插件缓存工具
schemas.ts -- 插件清单 Schema
walkPluginMarkdown.ts -- 插件 Markdown 文件遍历
15.2.2 BuiltinPluginDefinition 类型
BuiltinPluginDefinition 定义在 src/types/plugin.ts 中,是内置插件的声明式描述:
typescript
// 文件: src/types/plugin.ts
export type BuiltinPluginDefinition = {
/** 插件名称(用于 `{name}@builtin` 标识符) */
name: string
/** 在 /plugin UI 中显示的描述 */
description: string
/** 可选版本字符串 */
version?: string
/** 此插件提供的 Skill */
skills?: BundledSkillDefinition[]
/** 此插件提供的 Hooks */
hooks?: HooksSettings
/** 此插件提供的 MCP 服务器 */
mcpServers?: Record<string, McpServerConfig>
/** 此插件是否可用(例如基于系统能力判断)。不可用的插件完全隐藏 */
isAvailable?: () => boolean
/** 用户未设置偏好前的默认启用状态(默认 true) */
defaultEnabled?: boolean
}
相比 BundledSkillDefinition,BuiltinPluginDefinition 是一个更高层次的抽象。一个插件可以同时包含多个 Skill、一组 Hooks 配置和多个 MCP 服务器------它们作为一个整体被启用或禁用。
15.2.3 LoadedPlugin:统一的插件表示
无论插件来自哪里(内置、Git 仓库、Marketplace),加载后都被统一表示为 LoadedPlugin:
typescript
// 文件: src/types/plugin.ts
export type LoadedPlugin = {
name: string
manifest: PluginManifest
path: string
source: string // 如 "my-plugin@marketplace-name"
repository: string
enabled?: boolean
isBuiltin?: boolean
sha?: string // Git commit SHA,用于版本锁定
commandsPath?: string // 插件命令路径
skillsPath?: string // 插件技能路径
hooksConfig?: HooksSettings
mcpServers?: Record<string, McpServerConfig>
lspServers?: Record<string, LspServerConfig>
settings?: Record<string, unknown>
}
source 字段使用 {name}@{marketplace} 格式作为插件的全局唯一标识符。对于内置插件,格式为 {name}@builtin。
15.2.4 插件生命周期:注册、启用/禁用、持久化
内置插件的注册发生在启动阶段,由 src/plugins/bundled/index.ts 中的 initBuiltinPlugins() 触发:
typescript
// 文件: src/plugins/bundled/index.ts
export function initBuiltinPlugins(): void {
// 当前是脚手架代码,准备将 bundled skill 迁移为
// 用户可切换的内置插件
}
插件的启用/禁用状态由用户设置管理,存储在 settings.json 的 enabledPlugins 字段中。getBuiltinPlugins() 函数在每次调用时根据用户偏好和插件默认状态计算最终的启用/禁用列表:
typescript
// 文件: src/plugins/builtinPlugins.ts
export function getBuiltinPlugins(): {
enabled: LoadedPlugin[]
disabled: LoadedPlugin[]
} {
const settings = getSettings_DEPRECATED()
const enabled: LoadedPlugin[] = []
const disabled: LoadedPlugin[] = []
for (const [name, definition] of BUILTIN_PLUGINS) {
// 不可用的插件完全跳过
if (definition.isAvailable && !definition.isAvailable()) continue
const pluginId = `${name}@${BUILTIN_MARKETPLACE_NAME}`
const userSetting = settings?.enabledPlugins?.[pluginId]
// 优先级:用户偏好 > 插件默认值 > true
const isEnabled =
userSetting !== undefined
? userSetting === true
: (definition.defaultEnabled ?? true)
// ... 构建 LoadedPlugin 并分入 enabled 或 disabled
}
return { enabled, disabled }
}
这个三层优先级设计值得注意:用户的显式设置覆盖一切;如果用户未设置,则使用插件自身声明的 defaultEnabled;如果连插件都没声明,默认为启用。
15.2.5 插件 Skill 到 Command 的转换
当内置插件的 Skill 需要暴露为命令时,skillDefinitionToCommand() 函数负责转换:
typescript
// 文件: src/plugins/builtinPlugins.ts
function skillDefinitionToCommand(definition: BundledSkillDefinition): Command {
return {
type: 'prompt',
name: definition.name,
// 注意这里的 source 是 'bundled' 而非 'builtin'
// 'builtin' 在 Command.source 中表示硬编码的 slash 命令
// 'bundled' 让这些 Skill 保留在 SkillTool 的列表中
source: 'bundled',
loadedFrom: 'bundled',
isEnabled: definition.isEnabled ?? (() => true),
// ... 其他字段映射
}
}
这里有一个微妙但重要的设计决策:source 被设为 'bundled' 而非 'builtin'。注释中解释了原因------'builtin' 在 Command.source 的语义中表示硬编码的内部 slash 命令(如 /help),使用 'bundled' 可以确保这些 Skill 出现在 SkillTool 的技能列表中,也不会被 prompt 截断机制误伤。用户可切换的特性通过 LoadedPlugin.isBuiltin 单独追踪。
15.2.6 PluginError:类型安全的错误处理
插件加载涉及大量可能出错的环节------网络超时、Git 认证失败、清单解析错误、MCP 配置无效等。PluginError 使用判别联合类型(discriminated union)来精确描述每种错误:
typescript
// 文件: src/types/plugin.ts
export type PluginError =
| { type: 'path-not-found'; source: string; path: string; component: PluginComponent }
| { type: 'git-auth-failed'; source: string; gitUrl: string; authType: 'ssh' | 'https' }
| { type: 'git-timeout'; source: string; gitUrl: string; operation: 'clone' | 'pull' }
| { type: 'manifest-parse-error'; source: string; parseError: string }
| { type: 'plugin-not-found'; source: string; pluginId: string; marketplace: string }
| { type: 'mcp-server-suppressed-duplicate'; source: string; serverName: string; duplicateOf: string }
| { type: 'dependency-unsatisfied'; source: string; dependency: string; reason: 'not-enabled' | 'not-found' }
// ... 20+ 种错误类型
每种错误类型都携带了丰富的上下文信息,getPluginErrorMessage() 函数可以为每种错误生成可读的消息。这种设计避免了基于字符串匹配的错误处理------当错误消息文本变化时不会导致匹配失败。
15.3 Slash 命令系统
15.3.1 commands.ts 的架构
src/commands.ts 是整个命令系统的中枢,负责注册、加载和管理所有可用命令。文件开头是一组超过 70 个命令的导入语句,它们按功能分为几大类别。
命令分类:
| 类别 | 示例 | 特点 |
|---|---|---|
| 会话管理 | /clear, /compact, /session, /resume |
控制对话流程 |
| 代码操作 | /review, /diff, /commit, /branch |
Git 和代码审查 |
| 配置管理 | /config, /memory, /permissions, /hooks |
系统设置 |
| 开发辅助 | /doctor, /debug, /status, /cost |
诊断和监控 |
| 扩展管理 | /skills, /plugin, /mcp, /reload-plugins |
扩展系统管理 |
| UI 控制 | /theme, /color, /vim, /keybindings |
界面定制 |
| 认证相关 | /login, /logout, /usage |
用户认证 |
| 实验特性 | /proactive, /voice, /bridge |
受特性开关保护 |
特性开关保护 的命令使用条件性 require 导入,确保相关代码在特性未启用时不会进入最终构建:
typescript
// 文件: src/commands.ts
const voiceCommand = feature('VOICE_MODE')
? require('./commands/voice/index.js').default
: null
const workflowsCmd = feature('WORKFLOW_SCRIPTS')
? require('./commands/workflows/index.js').default
: null
15.3.2 Command 类型定义
Command 是一个由 CommandBase 和三种具体命令类型组成的联合类型:
typescript
// 文件: src/types/command.ts
export type CommandBase = {
availability?: CommandAvailability[]
description: string
hasUserSpecifiedDescription?: boolean
isEnabled?: () => boolean
isHidden?: boolean
name: string
aliases?: string[]
whenToUse?: string
disableModelInvocation?: boolean
userInvocable?: boolean
loadedFrom?: 'commands_DEPRECATED' | 'skills' | 'plugin' | 'managed' | 'bundled' | 'mcp'
kind?: 'workflow'
immediate?: boolean
userFacingName?: () => string
}
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)
三种命令类型对应三种执行模式:
- PromptCommand:返回 prompt 内容供模型处理,是 Skill 的载体
- LocalCommand :在本地同步执行,返回文本结果(如
/cost显示费用) - LocalJSXCommand :渲染 React/Ink 组件,提供交互式 TUI 界面(如
/config配置菜单)
availability 字段是一个新的设计,用于声明命令对哪些认证环境可用。例如,某些命令仅对 claude.ai 订阅用户开放,另一些仅对 Console API 直接用户开放:
typescript
// 文件: src/types/command.ts
export type CommandAvailability =
| 'claude-ai' // claude.ai OAuth 订阅用户
| 'console' // Console API key 直接用户
15.3.3 命令的执行流程
当用户输入 /xxx 或模型通过 SkillTool 调用技能时,命令执行经过以下关键阶段:
rust
输入解析 -> 命令查找 -> 可用性检查 -> 启用检查 -> 类型分发 -> 执行
命令查找 由 findCommand() 实现,支持按名称、别名和 userFacingName 匹配:
typescript
// 文件: src/commands.ts
export function findCommand(
commandName: string,
commands: Command[],
): Command | undefined {
return commands.find(
_ =>
_.name === commandName ||
getCommandName(_) === commandName ||
_.aliases?.includes(commandName),
)
}
可用性过滤 确保命令只在满足认证条件时可见。值得注意的是,这个检查不会被 memoize------因为认证状态可能在会话中途变化(如执行 /login 后):
typescript
// 文件: src/commands.ts
export async function getCommands(cwd: string): Promise<Command[]> {
const allCommands = await loadAllCommands(cwd) // memoized
const dynamicSkills = getDynamicSkills()
// 可用性和启用检查每次都重新评估
return allCommands.filter(
_ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
)
}
动态 Skill 是一个有趣的特性:在文件操作过程中,系统可能发现新的 Skill 文件并动态加入列表。getDynamicSkills() 获取这些运行时发现的 Skill,并在合适的位置插入到命令列表中------在插件 Skill 之后、内置命令之前。
15.3.4 SkillTool 与 Slash 命令的桥接
SkillTool 是模型自动调用 Skill 的入口,而 Slash 命令是用户手动调用的入口。两者最终都走向同一套 Command 注册表,但暴露的命令集合有所不同:
typescript
// 文件: src/commands.ts
// SkillTool 展示的命令:所有 prompt 类型、允许模型调用、非 builtin 的命令
export const getSkillToolCommands = memoize(async (cwd: string) => {
const allCommands = await getCommands(cwd)
return allCommands.filter(cmd =>
cmd.type === 'prompt' &&
!cmd.disableModelInvocation &&
cmd.source !== 'builtin' &&
(cmd.loadedFrom === 'bundled' || cmd.hasUserSpecifiedDescription || cmd.whenToUse)
)
})
这个过滤逻辑确保了几个关键约束:
- 内置的交互式命令(如
/config、/help)不暴露给模型 - 没有描述的 Skill 不出现在模型的技能列表中(避免占用 token 预算却无法被有效匹配)
- 标记了
disableModelInvocation的命令只能用户手动触发
15.4 Hooks 系统
Hooks 在工具执行的关键节点上提供拦截能力。下图展示了四种 Hook 类型在工具执行生命周期中的触发时机:
15.4.1 Hooks 的定位
如果说 Skill 定义了"做什么",Hooks 则定义了"什么时候额外做点什么"。Hooks 系统允许在 Claude Code 的关键生命周期节点上挂载自定义逻辑------在工具调用之前验证输入、在工具调用之后检查输出、在会话开始时初始化环境、在会话结束时清理资源。
15.4.2 Hook 事件类型
Hook 的事件类型覆盖了系统的几乎所有关键阶段。以下是主要的事件及其用途:
| 事件名称 | 触发时机 | 典型用途 |
|---|---|---|
PreToolUse |
工具调用之前 | 验证输入、修改参数、阻止调用 |
PostToolUse |
工具调用之后 | 检查输出、注入额外上下文 |
PostToolUseFailure |
工具调用失败后 | 错误恢复、降级处理 |
PermissionDenied |
自动模式拒绝工具调用后 | 允许重试 |
PermissionRequest |
权限请求时 | 自动审批/拒绝 |
SessionStart |
会话开始时 | 环境初始化 |
SessionEnd |
会话结束时 | 资源清理 |
Notification |
通知发送时 | 自定义通知渠道 |
UserPromptSubmit |
用户提交提示时 | 输入预处理 |
Stop |
模型停止时 | 后处理逻辑 |
SubagentStart/Stop |
子 Agent 启动/停止时 | Agent 监控 |
每个事件都支持 matcher 模式匹配。例如,PreToolUse 事件的 matcher 匹配 tool_name 字段,你可以配置一个 Hook 只在 Bash 工具被调用时触发。
15.4.3 四种 Hook 类型
Hook 的类型定义在 src/schemas/hooks.ts 中,使用 Zod 的判别联合模式:
typescript
// 文件: src/schemas/hooks.ts
export const HookCommandSchema = lazySchema(() => {
return z.discriminatedUnion('type', [
BashCommandHookSchema, // Shell 命令
PromptHookSchema, // LLM 提示
AgentHookSchema, // Agent 验证器
HttpHookSchema, // HTTP 回调
])
})
1. Command Hook(Shell 命令)
最基础的 Hook 类型,执行一个 Shell 命令并根据退出码决定后续行为:
typescript
const BashCommandHookSchema = z.object({
type: z.literal('command'),
command: z.string(), // 要执行的 Shell 命令
if: IfConditionSchema(), // 条件过滤(使用权限规则语法)
shell: z.enum(SHELL_TYPES).optional(), // Shell 类型
timeout: z.number().positive().optional(),
once: z.boolean().optional(), // 执行一次后自动移除
async: z.boolean().optional(), // 后台异步执行
asyncRewake: z.boolean().optional(), // 异步执行,exit code 2 时唤醒模型
})
退出码语义设计精巧:
- 退出码 0:成功,stdout/stderr 记录在后台
- 退出码 2:阻塞错误,stderr 内容展示给模型并阻止工具调用
- 其他退出码:非阻塞错误,stderr 仅展示给用户
2. Prompt Hook(LLM 提示)
使用 LLM 评估一段 prompt,适合需要智能判断的场景:
typescript
const PromptHookSchema = z.object({
type: z.literal('prompt'),
prompt: z.string(), // 使用 $ARGUMENTS 占位符获取 Hook 输入
model: z.string().optional(), // 模型覆盖
timeout: z.number().positive().optional(),
})
3. Agent Hook(Agent 验证器)
启动一个完整的 Agent 来执行验证,适合复杂的验证任务:
typescript
const AgentHookSchema = z.object({
type: z.literal('agent'),
prompt: z.string(), // 描述验证要求
model: z.string().optional(),
timeout: z.number().positive().optional(),
})
4. HTTP Hook(HTTP 回调)
向指定 URL 发送 POST 请求,适合与外部服务集成:
typescript
const HttpHookSchema = z.object({
type: z.literal('http'),
url: z.string().url(),
headers: z.record(z.string(), z.string()).optional(),
allowedEnvVars: z.array(z.string()).optional(), // 允许在 header 中引用的环境变量
})
allowedEnvVars 字段的设计体现了安全意识:header 中的 $VAR_NAME 引用只会解析列表中明确声明的环境变量,避免意外泄漏敏感信息。
15.4.4 HooksSettings 与 Matcher 机制
Hooks 的配置结构是一个以事件名称为 key、以 matcher 数组为 value 的部分记录:
typescript
// 文件: src/schemas/hooks.ts
export const HookMatcherSchema = lazySchema(() =>
z.object({
matcher: z.string().optional(), // 匹配模式
hooks: z.array(HookCommandSchema()), // 匹配时执行的 Hook 列表
}),
)
export const HooksSchema = lazySchema(() =>
z.partialRecord(z.enum(HOOK_EVENTS), z.array(HookMatcherSchema())),
)
export type HooksSettings = Partial<Record<HookEvent, HookMatcher[]>>
一个实际的 Hooks 配置示例:
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo '检查 Bash 命令安全性' && validate-bash-input.sh",
"if": "Bash(rm *)"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "eslint --fix $TOOL_INPUT_FILE_PATH"
}
]
}
]
}
}
if 条件使用权限规则语法(如 "Bash(git *)" 匹配所有以 git 开头的 Bash 命令),在实际执行 Hook 之前过滤不匹配的调用,避免为每次工具调用都启动进程。
15.4.5 Hook 的执行与结果处理
Hook 的执行由 src/utils/hooks.ts 和 src/services/tools/toolHooks.ts 协同完成。Pre/Post Tool Use Hooks 的执行流程如下:
lua
工具调用请求到达
|
v
executePreToolHooks()
-- 遍历所有 PreToolUse matcher
-- 对匹配的 matcher 执行其 hooks
-- 收集结果
|
v
结果聚合
-- 有阻塞错误? -> 阻止工具调用,错误信息反馈给模型
-- 有权限决策? -> 影响权限判断(approve/block/passthrough)
-- 有 updatedInput? -> 修改工具输入参数
|
v
工具实际执行
|
v
executePostToolHooks()
-- 遍历所有 PostToolUse matcher
-- 对匹配的 matcher 执行其 hooks
-- 结果可以:注入额外上下文、阻止后续推理、更新 MCP 工具输出
Hook 的结果通过 JSON 输出协议与系统通信。同步 Hook 可以返回丰富的结构化数据:
typescript
// 文件: src/types/hooks.ts
export type HookResult = {
message?: Message // 要添加到对话中的消息
blockingError?: HookBlockingError // 阻塞错误信息
outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
preventContinuation?: boolean // 是否阻止后续推理
stopReason?: string // 停止原因
permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
additionalContext?: string // 注入到对话中的额外上下文
updatedInput?: Record<string, unknown> // 修改后的工具输入
updatedMCPToolOutput?: unknown // 修改后的 MCP 工具输出
}
15.4.6 Skill 级别的 Hooks 注册
Skill 不仅可以定义 prompt 内容,还可以在 frontmatter 中声明 Hooks,这些 Hooks 在 Skill 被调用时动态注册到会话中。这个机制由 src/utils/hooks/registerSkillHooks.ts 实现:
typescript
// 文件: src/utils/hooks/registerSkillHooks.ts
export function registerSkillHooks(
setAppState: (updater: (prev: AppState) => AppState) => void,
sessionId: string,
hooks: HooksSettings,
skillName: string,
skillRoot?: string,
): void {
for (const eventName of HOOK_EVENTS) {
const matchers = hooks[eventName]
if (!matchers) continue
for (const matcher of matchers) {
for (const hook of matcher.hooks) {
// once: true 的 Hook 执行一次后自动移除
const onHookSuccess = hook.once
? () => removeSessionHook(setAppState, sessionId, eventName, hook)
: undefined
addSessionHook(setAppState, sessionId, eventName, matcher.matcher || '', hook, onHookSuccess, skillRoot)
}
}
}
}
这种设计使得 Skill 可以在执行期间临时增强系统的行为。例如,一个部署 Skill 可以注册一个 PostToolUse Hook,在每次 Bash 命令执行后检查部署状态;并且通过 once: true 声明,确保 Hook 只执行一次就自动清除,不会污染后续的对话。
15.4.7 超时与后台执行
Hook 的超时管理涉及两个层面:
typescript
// 文件: src/utils/hooks.ts
const TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000 // 工具 Hook: 10 分钟
const SESSION_END_HOOK_TIMEOUT_MS_DEFAULT = 1500 // 会话结束 Hook: 1.5 秒
常规 Hook 有 10 分钟的慷慨超时,因为它们可能执行编译、测试等耗时操作。但 SessionEnd Hook 的超时仅为 1.5 秒------这是因为会话结束时系统正在关闭,不能让 Hook 无限期阻塞退出流程。用户可以通过 CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS 环境变量调整这个值。
异步 Hook 通过 async: true 声明后台执行,不阻塞主流程。更高级的 asyncRewake: true 选项允许异步 Hook 在退出码为 2 时"唤醒"模型------它将结果作为一条通知消息排入消息队列,模型在空闲时或下一次查询时处理。
15.5 三者的关系与协作
15.5.1 架构全景图
sql
+-------------------------------------------------------------+
| 用户界面层 |
| Slash 命令 (/xxx) SkillTool (模型自动调用) |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| 统一命令注册表 (commands.ts) |
| getCommands() -> filter(availability) -> filter(isEnabled) |
+-------------------------------------------------------------+
| | | |
v v v v
+----------+ +----------+ +----------+ +----------+
| Bundled | |FileSystem| | Plugin | | MCP |
| Skills | | Skills | | Skills | | Skills |
+----------+ +----------+ +----------+ +----------+
| 编译进 | | .claude/ | | Git仓库/ | | MCP协议 |
| 二进制 | | skills/ | | 市场插件 | | 远程加载 |
+----------+ +----------+ +----------+ +----------+
|
v
+------------------+
| Plugin System |
| (打包多种组件) |
+------------------+
| - Skills |
| - Hooks |
| - MCP Servers |
| - LSP Servers |
| - Output Styles |
+------------------+
|
v
+-------------------------------------------------------------+
| Hooks 系统 (hooks.ts) |
| PreToolUse -> 工具执行 -> PostToolUse |
| SessionStart -> ... -> SessionEnd |
| 来源: settings.json / Skill frontmatter / Plugin hooksConfig|
+-------------------------------------------------------------+
15.5.2 从声明到执行的完整链路
下图展示了 SkillTool 从接收模型调用到执行 Skill 内容的完整决策流程:
让我们追踪一个完整的例子,展示三个系统如何协同工作。假设用户安装了一个名为 my-linter 的插件,它包含一个 Skill 和一个 Hook:
阶段一:启动时加载
loadAllCommands()并行加载所有命令源getPluginSkills()从插件的skills/目录加载 Markdown 文件- 解析 frontmatter,调用
createSkillCommand()创建Command对象 - 插件的
hooksConfig被加载到LoadedPlugin中
阶段二:模型发现 Skill
- SkillTool 的 prompt 包含技能列表,模型看到
my-linter:lint技能 - 系统级 prompt 中通过
system-reminder通知模型可用的 Skill
阶段三:Skill 执行
- 模型调用
SkillTool({ skill: "my-linter:lint", args: "src/" }) validateInput()验证技能存在且允许模型调用checkPermissions()检查权限规则call()根据context决定 inline 或 fork 执行- 如果 Skill 的 frontmatter 声明了 Hooks,
registerSkillHooks()将其注册到会话中
阶段四:Hooks 介入
- Skill prompt 指导模型使用
Write工具修改文件 PreToolUseHook 检查写入路径是否安全- 工具执行完成后,
PostToolUseHook 自动运行 linter - linter 发现问题时(退出码 2),错误信息反馈给模型
- 模型根据反馈修正代码
15.5.3 扩展性设计原则
Claude Code 的扩展架构遵循几个核心设计原则:
声明式优先:Skill 通过 Markdown + frontmatter 声明,Hook 通过 JSON 配置声明,插件通过 manifest 声明。尽可能避免要求用户编写程序代码。
渐进复杂度:最简单的扩展只需一个 Markdown 文件(自定义 Skill);需要更多控制时可以添加 frontmatter 声明 Hooks 和工具限制;需要完整打包分发时封装为插件;需要与外部系统集成时使用 HTTP Hook 或 MCP 服务器。
安全默认值:
- Skill 的工具权限默认为空,需要显式声明
allowed-tools - 新的
Command属性默认不在安全白名单中,需要显式审核 - 插件的
defaultEnabled默认为true,但isAvailable可以基于系统能力动态判断 - Hook 的
if条件使用权限规则语法精确过滤,避免不必要的进程启动
来源追踪 :每个 Command 都携带 source 和 loadedFrom 字段,用于遥测上报、权限判断和 UI 展示。插件命令还额外携带 pluginInfo,记录来源仓库和清单信息。
容错降级 :每层加载逻辑都有独立的错误处理,一个插件加载失败不会影响其他插件。PluginError 的判别联合类型确保每种错误都能被精确处理并给出有针对性的用户提示。
小结
本章深入剖析了 Claude Code 的三层扩展体系------Skill 系统、Plugin 系统和 Hooks 系统,以及将它们统一起来的 Slash 命令架构。
Skill 系统 是扩展的基础单元,将"能力"抽象为一段声明式的 prompt 内容加上元数据。通过 BundledSkillDefinition 类型和 registerBundledSkill() 注册机制,内置 Skill 编译进二进制中随 CLI 分发;通过 loadSkillsDir.ts 的磁盘加载器,用户可以在 .claude/skills/ 目录中用 Markdown 文件自由添加新技能;通过 MCP 协议,还可以从远程服务器加载技能。所有来源的 Skill 最终统一转换为 Command 对象,融入同一套命令注册表。
Plugin 系统 在 Skill 之上增加了组件化封装层,一个插件可以同时捆绑 Skill、Hooks、MCP 服务器和 LSP 服务器。BuiltinPluginDefinition 和 LoadedPlugin 类型分别表示内置插件定义和加载后的统一插件表示。三层启用/禁用优先级(用户偏好 > 插件默认 > 系统默认)确保了灵活性,而 20+ 种精确类型化的 PluginError 则为复杂的加载流程提供了可靠的错误处理。
Hooks 系统 深入到工具执行的生命周期中,支持 command、prompt、agent、http 四种 Hook 类型,覆盖了从 PreToolUse 到 SessionEnd 的 20 余种事件。通过 matcher 机制精确过滤触发条件,通过退出码语义约定传递执行结果,通过 JSON 输出协议支持阻塞、修改输入、注入上下文等丰富的交互模式。
Slash 命令系统 是统一的入口层,commands.ts 管理着 80+ 条命令的注册、加载、过滤和分发。getCommands() 函数融合了 Bundled Skill、文件系统 Skill、插件命令、工作流命令和内置命令六种来源,通过可用性检查和启用状态过滤确保每个用户看到的都是正确的命令集合。SkillTool 则是模型侧的入口,将 Skill 的能力暴露为模型可以自主调用的工具。
这套三层递进的扩展架构,既保证了系统的安全性和一致性,又为开发者和社区提供了灵活的扩展点。从一个简单的 Markdown 文件到一个完整的插件包,从一条 Shell 命令钩子到一个 Agent 级别的验证器,Claude Code 的扩展能力始终在"声明式简洁"和"程序化灵活"之间找到了恰到好处的平衡。