第 12 章:可扩展性——技能与钩子

扩展的两个维度

每个可扩展性系统都要回答两个问题:系统能做什么,以及它什么时候做。大多数框架会把二者混在一起——一个 plugin 在同一个对象中同时注册能力和生命周期回调,“添加功能”和“拦截功能”之间的边界被模糊成一个注册 API。

Claude Code 把它们干净地分开。Skills 扩展模型能做什么。它们是 markdown 文件,会变成 slash commands,在被调用时向对话注入新指令。Hooks 扩展事情何时以及如何发生。它们是生命周期拦截器,会在会话期间二十多个不同点触发,运行任意代码来阻止动作、修改输入、强制继续,或静默观察。

这种分离不是偶然的。Skills 是内容——它们通过添加 prompt text 扩展模型的知识和能力。Hooks 是控制流——它们在不改变模型所知内容的情况下修改执行路径。一个 skill 可能教模型如何运行团队部署流程。一个 hook 可能确保没有通过测试套件前不执行任何部署命令。skill 增加能力;hook 增加约束。

本章会深入介绍两个系统,然后考察它们的交汇点:由 skill 声明的 hooks 会在 skill 被调用时注册为 session-scoped lifecycle interceptors。


Skills:教模型新招式

两阶段加载

skills 系统的核心优化是:启动时只加载 frontmatter,完整内容只在调用时加载。

Phase 1 读取每个 SKILL.md 文件,把 YAML frontmatter 与 markdown body 分离,并提取 metadata。frontmatter 字段会成为系统提示的一部分,让模型知道该 skill 存在。markdown body 被捕获在 closure 中,但不处理。一个有 50 个 skills 的项目,只支付 50 条短描述的 token 成本,而不是 50 份完整文档的成本。

Phase 2 在模型或用户调用 skill 时触发。getPromptForCommand 会前置 base directory、替换变量($ARGUMENTS${CLAUDE_SKILL_DIR}${CLAUDE_SESSION_ID}),并执行 inline shell commands(用 ! 作为反引号前缀)。结果作为 content blocks 返回并注入对话。

七个来源及优先级

Skills 来自七个不同来源,并行加载后按优先级合并:

优先级来源位置说明
1Managed (Policy)<MANAGED_PATH>/.claude/skills/企业控制
2User~/.claude/skills/个人,全局可用
3Project.claude/skills/(向上走到 home)提交到版本控制
4Additional Dirs<add-dir>/.claude/skills/通过 --add-dir flag
5Legacy Commands.claude/commands/向后兼容
6Bundled编译进二进制受 feature gate 控制
7MCPMCP server prompts远程,不受信任

去重使用 realpath 解析 symlinks 和重叠父目录。先看到的来源获胜。getFileIdentity 函数通过 realpath 解析 canonical paths,而不是依赖 inode values,因为后者在 container/NFS mounts 和 ExFAT 上不可靠。

Frontmatter 契约

控制 skill 行为的关键 frontmatter 字段:

YAML 字段目的
name面向用户的显示名
description显示在 autocomplete 和系统提示中
when_to_use供模型发现用的详细使用场景
allowed-toolsskill 可使用哪些工具
disable-model-invocation阻止模型自主使用
context'fork' 表示作为子智能体运行
hooks调用时注册的生命周期 hooks
paths条件激活用的 glob patterns

context: 'fork' 选项会把 skill 作为子智能体运行,拥有自己的 context window。这对需要大量工作但不应污染主对话 token 预算的 skills 至关重要。disable-model-invocationuser-invocable 字段控制两条不同访问路径——把二者都设为 true 会让 skill 不可见,这对 hooks-only skills 很有用。

MCP 安全边界

变量替换之后,inline shell commands 会执行。安全边界是绝对的:MCP skills 永远不会执行 inline shell commands。 MCP servers 是外部系统。如果允许,一个包含 !`rm -rf /` 的 MCP prompt 会以用户完整权限执行。系统把 MCP skills 当作 content-only。这个信任边界连接到第 15 章讨论的更广泛 MCP 安全模型。

动态发现

Skills 不只在启动时加载。当模型触碰文件时,discoverSkillDirsForPaths 会从每个路径向上查找 .claude/skills/ 目录。带 paths frontmatter 的 skills 存储在 conditionalSkills map 中,并且只有在被触碰路径匹配其 patterns 时激活。一个声明 paths: "packages/database/**" 的 skill 在模型读取或编辑数据库文件之前保持不可见——这是上下文敏感的能力扩展。


Hooks:控制事情何时发生

Hooks 是 Claude Code 在生命周期点拦截和修改行为的机制。主执行引擎超过 4,900 行。系统服务三类受众:个人开发者(自定义 linting、validation)、团队(提交到项目的共享质量门禁)和企业(policy-managed compliance rules)。

真实 Hook:防止提交到 main

在深入机制之前,先看一个实际 hook。假设团队想防止模型直接 commit 到 main 分支。

第 1 步:settings.json 配置:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/check-not-main.sh",
            "if": "Bash(git commit*)"
          }
        ]
      }
    ]
  }
}

第 2 步:shell script:

#!/bin/bash
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ "$BRANCH" = "main" ]; then
  echo "Cannot commit directly to main. Create a feature branch first." >&2
  exit 2  # Exit 2 = blocking error
fi
exit 0

第 3 步:模型体验。 当模型尝试在 main 分支上执行 git commit 时,hook 会在命令执行前触发。script 检查分支,写入 stderr,并以 code 2 退出。模型看到一条 system message:“Cannot commit directly to main. Create a feature branch first.” commit 从未运行。模型会创建分支,并在那里 commit。

if: "Bash(git commit*)" 条件意味着 script 只对 git commit 命令运行——不是每次 Bash 调用都运行。Exit code 2 会阻塞;exit code 0 通过;任何其他 exit code 产生 non-blocking warning。这就是完整协议。

四种用户可配置类型

Claude Code 定义了六种 hook type——四种用户可配置,两种内部。

Command hooks 派生 shell process。Hook input JSON 通过 stdin 传入;hook 通过 exit code 和 stdout/stderr 回传。这是主力类型。

Prompt hooks 发起单次 LLM 调用,返回 {"ok": true}{"ok": false, "reason": "..."}。这提供轻量 AI-powered validation,不需要完整 agent loop。

Agent hooks 运行一个多轮 agentic loop(最多 50 turns,dontAsk 权限,禁用 thinking)。每个 hook 有自己的 session scope。这是“验证测试套件通过并覆盖新功能”的重型机制。

HTTP hooks 把 hook input POST 到 URL。无需本地进程派生即可启用远程 policy servers 和 audit logging。

两种内部类型是 callback hooks(程序化注册,通过跳过 span tracking 的 fast path 在 hot path 上降低 70% 开销)和 function hooks(用于 agent hooks 中结构化输出强制的 session-scoped TypeScript callbacks)。

五个最重要的生命周期事件

hook 系统会在二十多个生命周期点触发。五个事件主导真实使用:

PreToolUse——每次工具执行前触发。可以阻塞、修改输入、自动批准或注入上下文。权限行为遵循严格优先级:deny > ask > allow。最常见的质量门禁 hook point。

PostToolUse——成功执行后触发。可以注入上下文,或完全替换 MCP tool output。适用于对工具结果的自动反馈。

Stop——Claude 得出响应结论前触发。blocking hook 会强制继续。这是自动验证循环的机制:“你真的完成了吗?”

SessionStart——会话开始时触发。可以设置环境变量、覆盖第一条用户消息,或注册 file watch paths。不能阻塞(hook 不能阻止会话启动)。

UserPromptSubmit——用户提交 prompt 时触发。可以阻塞处理,让输入验证或内容过滤在模型看到之前发生。

参考表——剩余事件:

类别事件
Tool lifecyclePostToolUseFailure, PermissionDenied, PermissionRequest
SessionSessionEnd (1.5s timeout), Setup
SubagentSubagentStart, SubagentStop
CompactionPreCompact, PostCompact
NotificationNotification, Elicitation, ElicitationResult
ConfigurationConfigChange, InstructionsLoaded, CwdChanged, FileChanged, TaskCreated, TaskCompleted, TeammateIdle

阻塞不对称是刻意的。代表可恢复决策的事件(tool calls、stop conditions)支持阻塞。代表不可逆事实的事件(session started、API failed)不支持。

Exit Code 语义

对 command hooks,exit codes 有特定含义:

Exit Code含义是否阻塞
0成功,如果 stdout 是 JSON 则解析
2Blocking error,stderr 作为 system message 显示
OtherNon-blocking warning,只显示给用户

Exit code 2 是刻意选择的。Exit code 1 太常见——任何未处理异常、断言失败或语法错误都会产生 exit 1。使用 exit 2 可以防止意外 enforcement。

六个 Hook 来源

来源信任级别说明
userSettingsUser~/.claude/settings.json,最高优先级
projectSettingsProject.claude/settings.json,版本控制
localSettingsLocal.claude/settings.local.json,gitignored
policySettingsEnterprise不能被覆盖
pluginHookPlugin优先级 999(最低)
sessionHookSession仅内存中,由 skills 注册

Snapshot 安全模型

Hooks 执行任意代码。项目的 .claude/settings.json 可以定义在每次工具调用前触发的 hooks。如果恶意仓库在用户接受 workspace trust dialog 后修改自己的 hooks,会发生什么?

什么都不会发生。hooks 配置在启动时被冻结。

captureHooksConfigSnapshot() 在启动期间调用一次。从那一刻起,executeHooks() 从 snapshot 读取,绝不会隐式重新读取 settings files。snapshot 只会通过显式通道更新:/hooks 命令或 file watcher detection,二者都会通过 updateHooksConfigSnapshot() 重建。

policy enforcement cascade:policy settings 中的 disableAllHooks 会清空一切。allowManagedHooksOnly 排除 user 和 project hooks。用户可以通过设置 disableAllHooks 禁用自己的 hooks,但不能禁用 enterprise-managed hooks。policy 层永远获胜。

trust check 本身(shouldSkipHookDueToTrust())是在两个漏洞后引入的:用户 拒绝 trust dialog 时 SessionEnd hooks 仍然执行,以及 SubagentStop hooks 在 trust 展示前触发。二者根因相同——hooks 在用户尚未同意 workspace code execution 的生命周期状态中触发。修复是在 executeHooks() 顶部加集中 gate。


执行流

内部 callbacks 的 fast path 是重要优化。当所有匹配 hooks 都是内部的(file access analytics、commit attribution)时,系统会跳过 span tracking、abort signal creation、progress messages 和完整 output processing pipeline。大多数 PostToolUse 调用只命中内部 callbacks。

Hook input JSON 会通过惰性的 getJsonInput() closure 序列化一次,并在所有并行 hooks 之间复用。环境注入会设置 CLAUDE_PROJECT_DIRCLAUDE_PLUGIN_ROOT,并在某些事件中设置 CLAUDE_ENV_FILE,hooks 可以往其中写入环境变量导出。


集成:Skills 与 Hooks 的交汇

当 skill 被调用时,它在 frontmatter 中声明的 hooks 会注册为 session-scoped hooks。skillRoot 会成为 hook shell commands 的 CLAUDE_PLUGIN_ROOT

my-skill/
  SKILL.md          # The skill content
  validate.sh       # Called by a PreToolUse hook declared in frontmatter

skill 的 frontmatter 声明:

hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "${CLAUDE_PLUGIN_ROOT}/validate.sh"
          once: true

用户调用 /my-skill 时,skill content 会加载进对话,PreToolUse hook 也会注册。下一次 Bash tool call 触发 validate.sh。由于设置了 once: true,hook 在第一次成功执行后会移除自己。

对 agents 来说,frontmatter 中声明的 Stop hooks 会自动转换为 SubagentStop hooks,因为 subagents 触发 SubagentStop,不是 Stop。没有这个转换,agent 的 stop-verification hook 永远不会触发。

权限行为优先级

executePreToolHooks() 可以阻塞(通过 blockingError)、自动批准(通过 permissionBehavior: 'allow')、强制询问(通过 'ask')、拒绝(通过 'deny')、修改输入(通过 updatedInput),或添加上下文(通过 additionalContext)。当多个 hooks 返回不同行为时,deny 永远获胜。对安全相关决策来说,这是正确默认值。

Stop Hooks:强制继续

当 Stop hook 返回 exit code 2 时,stderr 会作为反馈显示给模型,对话继续。这把一次性 prompt-response 转变成目标导向循环。Stop hook 可以说是整个系统中最强大的集成点。


应用到你的系统:设计可扩展性系统

把内容与控制流分开。 Skills 增加能力;hooks 约束行为。混合二者会让人无法推理 plugin 做了什么、阻止了什么。

在信任边界冻结配置。 snapshot 机制在同意时捕获 hooks,并且永不隐式重新读取。如果你的系统执行用户提供的代码,这可以消除 TOCTOU attacks。

用不常见的 exit codes 表示语义信号。 Exit code 1 是噪音——每个未处理错误都会产生它。把 exit code 2 作为阻塞信号,可以防止意外 enforcement。选择需要明确意图的信号。

在 socket 层验证,而不是应用层。 SSRF guard 在 DNS lookup 时运行,而不是预检查。这消除了 DNS rebinding window。验证网络目标时,检查必须与连接是原子的。

针对常见情况优化。 内部 callback fast path(-70% overhead)认识到大多数 hook invocations 只命中内部 callbacks。两阶段 skill loading 认识到给定会话中大多数 skills 永远不会被调用。每个优化都针对真实使用分布。

可扩展性系统体现了对能力与安全之间张力的成熟理解。Skills 给予模型新能力,并受 MCP 安全线约束(第 15 章)。Hooks 让外部代码影响模型行动,并受 snapshot 机制、exit code 语义和 policy cascade 约束。两个系统互不信任——而这种相互不信任正是组合能够在规模化部署中保持安全的原因。

下一章转向视觉层:Claude Code 如何以 60fps 渲染响应式终端 UI,并跨五种终端协议处理输入。