Claude Code 架构全解Claude Code Architecture, in Full第 9 章 · 14 章Chapter 9 of 14
全文目录Contents
  1. 0 · 项目全景与代码地图
    1. 0.1 这个软件是什么
    2. 0.2 源码目录逐个解释
    3. 0.3 从这张地图能读出的三件事
    4. 0.4 一次完整请求的旅程(全文导航)
    5. 0.5 全文章节索引
  2. 1 · 入口层与启动流程
    1. 1.1 四种启动形态
    2. 1.2 启动的第一个设计:快路径分派
    3. 1.3 命令行参数:60 多个选项
    4. 1.4 --bare:一个值得单独讲的极简模式
    5. 1.5 启动时序
    6. 1.6 系统提示词的三段结构
  3. 2 · 会话层:QueryEngine
    1. 2.1 它解决什么问题
    2. 2.2 它持有哪些状态
    3. 2.3 一次 submitMessage 的完整流程
    4. 2.4 用户消息为什么必须先落盘
    5. 2.5 消费主循环输出:一个大 switch
    6. 2.6 压缩分界点:主动释放内存
    7. 2.7 三种退出结果
    8. 2.8 ask():一次性调用的便捷封装
  4. 3 · 智能体主循环 ★
    1. 3.1 循环的骨架
    2. 3.2 State:把跨轮次状态集中管理
    3. 3.3 transition:只为可测试而存在的字段
    4. 3.4 七条转移边逐条详解
    5. 3.5 错误扣留机制
    6. 3.6 中断处理
    7. 3.7 模型降级:三个动作
    8. 3.8 思考块三定律
    9. 3.9 循环里的其他机制
    10. 3.10 循环的所有退出点
  5. 4 · 工具模型
    1. 4.1 Tool 接口:七组正交能力
    2. 4.2 为什么「安全谓词」值得单独成组
    3. 4.3 失败保守默认值
    4. 4.4 40 个内建工具分类
    5. 4.5 渐进式工具加载
    6. 4.6 工具清单装配:一个关于缓存的隐藏约束
    7. 4.7 backfillObservableInput:一个极致的缓存保护例子
  6. 5 · 工具执行
    1. 5.1 执行链路总览
    2. 5.2 并发分区:贪心算法
    3. 5.3 上下文修改要排队到批次结束
    4. 5.4 单次执行:runToolUse 的完整流程
    5. 5.5 流式工具执行器
    6. 5.6 兄弟中止控制器:最漂亮的一处设计
    7. 5.7 丢弃机制
    8. 5.8 「墓碑」消息
    9. 5.9 结果的最终处理
  7. 6 · 上下文治理 ★
    1. 6.1 五级流水线
    2. 6.2 第 ① 级:工具结果预算
    3. 6.3 第 ③ 级:微压缩与缓存编辑
    4. 6.4 第 ⑤ 级:自动摘要压缩
    5. 6.5 上下文真的超了:三级恢复瀑布
  8. 7 · 权限系统
    1. 7.1 六种权限模式
    2. 7.2 十步决策级联
    3. 7.3 自动模式:模型分类器 + 三级快速通道
    4. 7.4 权限规则的语法
    5. 7.5 沙箱与只读命令判定
    6. 7.6 权限判定的完整数据结构
    7. 7.7 权限决策的可解释性
  9. 8 · 子智能体
    1. 8.1 首要动机是上下文隔离,不是并行
    2. 8.2 三种形态
    3. 8.3 分叉:把提示词缓存用到极致
    4. 8.4 子智能体的工具限制
    5. 8.5 子智能体的上下文构造
    6. 8.6 内建智能体类型
    7. 8.7 后台任务的几种形态
    8. 8.8 中断的级联
  10. 9 · 扩展体系
    1. 9.1 四类扩展点对照
    2. 9.2 技能系统
    3. 9.3 插件系统
    4. 9.4 MCP 客户端
    5. 9.5 钩子:15 类生命周期事件
    6. 9.6 输出样式
  11. 10 · 终端界面层
    1. 10.1 用 React 写终端界面
    2. 10.2 最大的四个组件
    3. 10.3 输入框为什么有 347 KB
    4. 10.4 虚拟消息列表
    5. 10.5 工具结果的六种渲染状态
    6. 10.6 折叠:避免刷屏
    7. 10.7 87 个状态管理单元
    8. 10.8 界面和内核的接口:ToolUseContext 里的回调
    9. 10.9 一个有趣的细节:ANSI 转 PNG
  12. 11 · 持久化与恢复
    1. 11.1 对话记录的格式:JSONL
    2. 11.2 对话记录是一棵树,不是一个列表
    3. 11.3 写入队列
    4. 11.4 子智能体的记录:支链文件
    5. 11.5 恢复:三种方式
    6. 11.6 文件历史:智能体改过的文件可以回滚
    7. 11.7 配置的多来源与迁移
    8. 11.8 记忆目录
  13. 12 · 可观测体系
    1. 12.1 埋点密度
    2. 12.2 事件命名
    3. 12.3 查询链路追踪
    4. 12.4 类型层面的隐私保护
    5. 12.5 缓存断裂检测
    6. 12.6 性能剖析检查点
    7. 12.7 慢操作日志
    8. 12.8 内存错误缓冲区
    9. 12.9 内部错误的响亮日志
    10. 12.10 埋点的成本意识
  14. 13 · 构建与分发
    1. 13.1 Bun 单文件可执行程序
    2. 13.2 编译期特性开关:89 个
    3. 13.3 死代码消除:为什么这不只是「if 判断」
    4. 13.4 编译期宏
    5. 13.5 运行时特性开关:另一套系统
    6. 13.6 版本与更新
    7. 13.7 从构建方式反推的架构约束
  1. 0 · The Project at a Glance, and a Map of the Code
    1. 0.1 What this software is
    2. 0.2 The source tree, directory by directory
    3. 0.3 Three things you can read off this map
    4. 0.4 The journey of one complete request (a map of the whole book)
    5. 0.5 Chapter index
  2. 1 · The Entry Layer and Startup
    1. 1.1 Four launch modes
    2. 1.2 The first design decision at startup: fast-path dispatch
    3. 1.3 Command-line arguments: 60-plus options
    4. 1.4 --bare: a minimal mode worth its own section
    5. 1.5 Startup sequence
    6. 1.6 The three-part structure of the system prompt
  3. 2 · The Session Layer: QueryEngine
    1. 2.1 The problem it solves
    2. 2.2 What state it holds
    3. 2.3 The full flow of one submitMessage
    4. 2.4 Why the user message must hit disk first
    5. 2.5 Consuming the main loop’s output: one big switch
    6. 2.6 The compact boundary: proactively releasing memory
    7. 2.7 Three kinds of exit result
    8. 2.8 ask(): a convenience wrapper for one-shot calls
  4. 3 · The Agent Main Loop ★
    1. 3.1 The skeleton of the loop
    2. 3.2 State: centralizing cross-iteration state
    3. 3.3 transition: a field that exists purely for testability
    4. 3.4 The seven transition edges, one by one
    5. 3.5 The error-withholding mechanism
    6. 3.6 Interrupt handling
    7. 3.7 Model fallback: three actions
    8. 3.8 The three laws of thinking blocks
    9. 3.9 Other mechanisms in the loop
    10. 3.10 Every exit point of the loop
  5. 4 · The Tool Model
    1. 4.1 The Tool interface: seven orthogonal capability groups
    2. 4.2 Why the “safety predicates” deserve their own group
    3. 4.3 Fail-safe defaults
    4. 4.4 The 40 built-in tools, by category
    5. 4.5 Progressive tool loading
    6. 4.6 Tool list assembly: a hidden constraint about caching
    7. 4.7 backfillObservableInput: an extreme example of cache protection
  6. 5 · Tool Execution
    1. 5.1 The execution pipeline at a glance
    2. 5.2 Concurrency partitioning: a greedy algorithm
    3. 5.3 Context modifications are queued until the batch ends
    4. 5.4 A single execution: the full flow of runToolUse
    5. 5.5 The streaming tool executor
    6. 5.6 The sibling abort controller: the most elegant design in the file
    7. 5.7 The discard mechanism
    8. 5.8 “Tombstone” messages
    9. 5.9 Final processing of results
  7. 6 · Context Management ★
    1. 6.1 The five-tier pipeline
    2. 6.2 Tier ①: the tool result budget
    3. 6.3 Tier ③: micro-compaction and cache edits
    4. 6.4 Tier ⑤: auto summary compaction
    5. 6.5 When the context really does overflow: the three-tier recovery cascade
  8. 7 · The Permission System
    1. 7.1 Six Permission Modes
    2. 7.2 The Ten-Step Decision Cascade
    3. 7.3 Auto Mode: Model Classifier + Three-Tier Fast Path
    4. 7.4 Permission Rule Syntax
    5. 7.5 Sandboxing and Read-Only Command Detection
    6. 7.6 The Full Data Structure Behind a Permission Decision
    7. 7.7 Explainability of Permission Decisions
  9. 8 · Subagents
    1. 8.1 The Primary Motivation Is Context Isolation, Not Parallelism
    2. 8.2 Three Forms
    3. 8.3 Forking: Pushing Prompt Caching to the Limit
    4. 8.4 Tool Restrictions on Subagents
    5. 8.5 Constructing a Subagent's Context
    6. 8.6 Built-in Agent Types
    7. 8.7 The Forms Background Tasks Take
    8. 8.8 Cascading Interruption
  10. 9 · The Extension System
    1. 9.1 The Four Extension Points Compared
    2. 9.2 The Skill System
    3. 9.3 The Plugin System
    4. 9.4 The MCP Client
    5. 9.5 Hooks: 15 Kinds of Lifecycle Events
    6. 9.6 Output Styles
  11. 10 · The Terminal UI Layer
    1. 10.1 Writing a Terminal UI in React
    2. 10.2 The Four Biggest Components
    3. 10.3 Why the Input Box Is 347 KB
    4. 10.4 The Virtualized Message List
    5. 10.5 Six Rendering States for Tool Results
    6. 10.6 Collapsing: Avoiding Screen Flood
    7. 10.7 87 State-Management Units
    8. 10.8 The Interface Between UI and Kernel: Callbacks in ToolUseContext
    9. 10.9 A Fun Detail: ANSI to PNG
  12. 11 · Persistence and Resume
    1. 11.1 The Transcript Format: JSONL
    2. 11.2 The Transcript Is a Tree, Not a List
    3. 11.3 The Write Queue
    4. 11.4 Subagent Records: Sidechain Files
    5. 11.5 Resume: Three Ways
    6. 11.6 File History: Rolling Back Files the Agent Changed
    7. 11.7 Multi-Source Settings and Migrations
    8. 11.8 The Memory Directory
  13. 12 · The Observability System
    1. 12.1 Instrumentation Density
    2. 12.2 Event Naming
    3. 12.3 Query Chain Tracing
    4. 12.4 Privacy Protection at the Type Level
    5. 12.5 Cache-Break Detection
    6. 12.6 Profiling Checkpoints
    7. 12.7 Slow-Operation Logging
    8. 12.8 The In-Memory Error Buffer
    9. 12.9 Loud Logging for Internal Errors
    10. 12.10 Cost-Awareness in Instrumentation
  14. 13 · Build and Distribution
    1. 13.1 The Bun Single-File Executable
    2. 13.2 Compile-Time Feature Flags: 89 of Them
    3. 13.3 Dead-Code Elimination: Why It's Not Just an “if”
    4. 13.4 Compile-Time Macros
    5. 13.5 Runtime Feature Flags: A Separate System
    6. 13.6 Versions and Updates
    7. 13.7 Architectural Constraints Inferred from the Build

9 · 扩展体系

「扩展点」的意思是:让第三方或用户自己,在不修改主程序源代码的前提下,往系统里添加能力。Claude Code 有四类扩展点,机制各不相同。

9.1 四类扩展点对照

类型形态谁触发能做什么
技能
Skill
Markdown 文件 模型(通过 SkillTool) 把一段固定的操作流程或专业知识,做成模型可以按需调用的能力
插件
Plugin
代码包 安装即生效 注册新的工具、斜杠命令、钩子、智能体类型
MCP 独立进程 / HTTP 服务 模型(工具调用) 接入外部系统的工具和资源,跨语言、跨进程
钩子
Hook
脚本 / 命令 系统在特定时机 在 15 个生命周期节点上拦截、修改、阻断

9.2 技能系统

形态

一个技能就是一个 SKILL.md 文件,头部有 YAML 元数据(业内叫 frontmatter):

~/.claude/skills/my-skill/SKILL.md --- name: my-skill description: 一句话说明这个技能干什么,什么时候该用 allowed-tools: Read, Bash(git:*) ← 可选:限制这个技能能用哪些工具 hooks: ... ← 可选:技能自带的钩子 --- # 技能正文 这里写详细的操作步骤、注意事项、示例…… 可以很长,几千 token 都没关系。

核心机制:渐进式披露

程序启动时 └─ 扫描技能目录,只解析每个文件的头部元数据 (name + description,每个几十 token) ★ 这部分是"常驻"的,每一轮都要重发 ↓ 模型判断某个技能可能相关时 └─ 调用 SkillTool(名字) ↓ 运行时 └─ 完整的 SKILL.md 正文才被注入上下文(可能几千 token)

Claude Code 有一个专门的函数量化常驻成本:

export function estimateSkillFrontmatterTokens(skill: Command): number

因为所有技能的头部元数据都常驻上下文,装 100 个技能的固定成本必须可测量 —— 否则用户装着装着就发现每轮都在白烧几千 token。

加载器的实现细节

// skills/loadSkillsDir.ts
export type LoadedFrom = ...                    // 从哪个来源加载的
export function getSkillsPath(...)              // 技能目录路径
export function estimateSkillFrontmatterTokens(skill: Command): number
function parseHooksFromFrontmatter(...)         // 解析技能自带的钩子
function parseSkillPaths(frontmatter): string[] | undefined
export function parseSkillFrontmatterFields(...)
export function createSkillCommand({...})       // 把技能包装成一个命令对象
function isSkillFile(filePath: string): boolean
function transformSkillFiles(files: MarkdownFile[]): MarkdownFile[]
function buildNamespace(targetDir: string, baseDir: string): string   // 命名空间
function getSkillCommandName(filePath: string, baseDir: string): string
export const getSkillDirCommands = memoize(...)  // ★ 结果被缓存
export function clearSkillCaches()

// 动态技能:运行时注册的,不在磁盘上
const dynamicSkillDirs  = new Set<string>()
const dynamicSkills     = new Map<string, Command>()

几个值得注意的点:

  • buildNamespace —— 技能有命名空间。放在 skills/git/commit/SKILL.md 的技能,名字会是 git:commit。避免不同来源的技能重名。
  • memoize —— 加载结果被缓存。技能目录扫描涉及大量文件读取,不能每次都做。配套有 clearSkillCaches()/reload 命令使用。
  • 动态技能 —— 可以在运行时注册技能,不需要写文件。插件和 MCP 服务可以用这个机制提供技能。

技能是「把命令变成工具」的桥

回顾第 0.3 节的那条切分线:tools/ 是模型能调的,commands/ 是只有人能敲的。

技能打破了这条界限 —— 它让一段「命令式」的内容(固定流程、专业知识)以工具的形式暴露给模型。而且暴露的成本很低,因为常驻的只有一句描述。

9.3 插件系统

utils/plugins/ 目录:

文件职责
pluginLoader.ts(107 KB)发现、加载、校验、注册插件
marketplaceManager.ts(91 KB)插件市场:浏览、安装、更新
schemas.ts(57 KB)插件清单文件的格式定义与校验

对应的斜杠命令在 commands/plugin/ 下:

  • ManagePlugins.tsx(314 KB)—— 插件管理界面
  • BrowseMarketplace.tsx(117 KB)—— 市场浏览界面
  • PluginSettings.tsx(126 KB)—— 插件设置

注意界面代码比逻辑代码还大。这是终端界面的典型特征 —— 在终端里画一个可交互的列表、处理键盘导航、渲染滚动条,代码量远超同样功能的网页版。

缓存优先加载

// QueryEngine.ts
// Cache-only: headless/SDK/CCR startup must not block on network for
// ref-tracked plugins. CCR populates the cache via CLAUDE_CODE_SYNC_PLUGIN_INSTALL
// (headlessPluginInstall) or CLAUDE_CODE_PLUGIN_SEED_DIR before this runs;
// SDK callers that need fresh source can call /reload-plugins.
const [skills, { enabled: enabledPlugins }] = await Promise.all([
  getSlashCommandToolSkills(getCwd()),
  loadAllPluginsCacheOnly(),        // ★ 只读缓存,不发网络请求
])

译:仅缓存模式:无头 / 开发工具包 / 远程环境的启动,不能因为要拉取「按引用追踪的插件」而阻塞在网络上。……需要最新源码的调用方可以执行 /reload-plugins 命令。

这是一个重要的启动性能约束:任何自动化场景下的启动都不能依赖网络。网络可能慢、可能不通、可能需要认证 —— 而一个跑在流水线里的任务不能因此卡死。

9.4 MCP 客户端

MCP 是 Model Context Protocol(模型上下文协议)的缩写,一个让智能体接入外部工具服务的开放标准。services/mcp/ 目录实现了客户端。

两种传输方式

方式说明
标准输入输出
stdio
Claude Code 启动一个子进程,通过它的标准输入输出通信。适合本地工具
HTTP连接一个网络服务。适合远程服务、需要认证的服务

工具名的前缀

MCP 工具的名字会被加上前缀:mcp__服务名__工具名。这样:

  • 不同服务提供的同名工具不会冲突
  • 权限规则可以按服务前缀批量配置(mcp__github 匹配该服务下所有工具)

但也有一个例外模式:环境变量 CLAUDE_AGENT_SDK_MCP_NO_PREFIX 可以关掉前缀。所以 Tool 接口里有一个专门的字段应对:

/**
 * For MCP tools: the server and tool names as received from the MCP server
 * (unnormalized). Present on all MCP tools regardless of whether `name` is
 * prefixed (mcp__server__tool) or unprefixed (CLAUDE_AGENT_SDK_MCP_NO_PREFIX mode).
 */
mcpInfo?: { serverName: string; toolName: string }

无论名字有没有前缀,原始的服务名和工具名都单独保存一份。这样权限判定、埋点、错误信息都能拿到准确的来源信息,不用去解析名字字符串。

向用户索取信息(Elicitation)

MCP 协议支持服务端反过来向用户要信息(比如「请输入你的 API 密钥」)。Claude Code 有专门的处理:

/**
 * Optional handler for URL elicitations triggered by tool call errors (-32042).
 * In print/SDK mode, this delegates to structuredIO.handleElicitation.
 * In REPL mode, this is undefined and the queue-based UI path is used.
 */
handleElicitation?: (
  serverName: string,
  params: ElicitRequestURLParams,
  signal: AbortSignal,
) => Promise<ElicitResult>

两条路径:交互模式下走界面队列弹对话框(对应的组件 ElicitationDialog.tsx 有 175 KB);无头模式下走结构化输入输出协议,把请求转发给外层调用方。

-32042 是 MCP 协议里的一个特定错误码,表示「我需要用户提供信息才能继续」。

MCP 相关的其他工具

  • ListMcpResourcesTool / ReadMcpResourceTool —— MCP 除了工具还能提供「资源」(可读的数据),这两个工具让模型访问它们
  • McpAuthTool —— 处理 OAuth 认证流程
  • ReadMcpResourceDirTool —— 列出资源目录(对声明支持的服务)

9.5 钩子:15 类生命周期事件

钩子让用户在系统的特定时机执行自己的脚本。这是最强大也最危险的扩展点 —— 因为钩子可以阻断操作、修改参数。

// types/hooks.ts 里的事件类型
hookEventName: z.literal('PreToolUse')          // 工具执行前
hookEventName: z.literal('PostToolUse')         // 工具执行后
hookEventName: z.literal('PostToolUseFailure')  // 工具执行失败后
hookEventName: z.literal('PermissionRequest')   // 权限请求时
hookEventName: z.literal('PermissionDenied')    // 权限被拒时
hookEventName: z.literal('UserPromptSubmit')    // 用户提交提问时
hookEventName: z.literal('SessionStart')        // 会话开始
hookEventName: z.literal('Setup')               // 初始化 / 维护
hookEventName: z.literal('SubagentStart')       // 子智能体启动
hookEventName: z.literal('Notification')        // 通知
hookEventName: z.literal('Elicitation')         // MCP 索取信息
hookEventName: z.literal('ElicitationResult')   // 索取结果
hookEventName: z.literal('CwdChanged')          // 工作目录变了
hookEventName: z.literal('FileChanged')         // 文件被外部修改
hookEventName: z.literal('WorktreeCreate')      // 创建工作树

claude-code/src/types/hooks.ts

另外还有几类在别处定义的:Stop(结束前)、PreCompact(压缩前)、PostSampling(模型采样后)。

钩子的执行引擎

utils/hooks/ 目录:

文件职责
execAgentHook.ts执行「智能体型」钩子 —— 钩子本身是一次模型调用
execHttpHook.ts执行 HTTP 钩子 —— 把事件 POST 到一个网址
execPromptHook.ts执行提示词钩子
ssrfGuard.ts服务端请求伪造防护 —— 防止 HTTP 钩子被诱导去访问内网地址
AsyncHookRegistry.ts异步钩子注册表
hookEvents.ts钩子执行的事件流(开始/进度/响应)
hooksConfigManager.ts / hooksConfigSnapshot.ts配置管理与快照
registerSkillHooks.ts / registerFrontmatterHooks.ts注册技能自带的钩子
fileChangedWatcher.ts文件变更监听
skillImprovement.ts技能自我改进

ssrfGuard.ts 的存在值得注意。HTTP 钩子会把事件内容发到用户配置的网址。如果不加防护,一个恶意的(或被诱导的)配置可以让 Claude Code 去访问 http://169.254.169.254/(云服务商的元数据接口,能拿到临时凭据)—— 这是经典的服务端请求伪造攻击。

钩子的进度反馈

export function startHookProgressInterval(params: {...}): ...
export const HOOK_TIMING_DISPLAY_THRESHOLD_MS = 500

钩子是用户自己写的脚本,耗时完全不可控。所以:

  • 超过 500 毫秒才显示计时(避免快钩子的界面闪烁)
  • 有一个定时器周期性发出进度事件,让用户知道「系统没卡死,是你的钩子在跑」

钩子的条件匹配

钩子配置可以带条件,比如「只在 Bash 工具执行 git 命令时触发」。这需要工具配合:

/**
 * Prepare a matcher for hook `if` conditions (permission-rule patterns like
 * "git *" from "Bash(git *)"). Called once per hook-input pair; any
 * expensive parsing happens here. Returns a closure that is called per
 * hook pattern. If not implemented, only tool-name-level matching works.
 */
preparePermissionMatcher?(input: z.infer<Input>): Promise<(pattern: string) => boolean>

注意设计:返回的是一个闭包,而不是直接做匹配。因为一个工具调用可能要对照几十条钩子模式,而解析(比如把 shell 命令解析成语法树)很贵。所以把「贵的准备工作」做一次,返回一个「便宜的匹配函数」重复调用

9.6 输出样式

outputStyles/ 是一个小但有意思的扩展点:允许用户替换系统提示词的「人格」部分

它在代码里的影响之一,是让查询来源标识变成动态的:

// Prefix-match because promptCategory.ts sets the querySource to
// 'repl_main_thread:outputStyle:<style>' when a non-default output style
// is active. The bare 'repl_main_thread' is only used for the default style.
function isMainThreadSource(querySource: QuerySource | undefined): boolean {
  return !querySource || querySource.startsWith('repl_main_thread')
}

注释里还提到了一个因此产生的 bug:

「query.ts:350/1451 use the same startsWith pattern; the pre-existing cached-MC === 'repl_main_thread' check was a latent bug — users with a non-default output style were silently excluded from cached MC.」

译:……之前缓存微压缩里那个「完全等于 repl_main_thread」的判断是一个潜伏的 bug —— 使用了非默认输出样式的用户被静默地排除在缓存微压缩之外。

这是一个典型的「特性交互 bug」:输出样式功能改了一个标识字符串的格式,而另一个完全不相关的功能(缓存微压缩)恰好在用精确匹配检查这个字符串。没有报错,没有告警 —— 只是那部分用户悄悄失去了一个优化。

9 · The Extension System

An “extension point” means: letting third parties, or users themselves, add capabilities to the system without modifying the main program's source code. Claude Code has four kinds of extension points, each with a different mechanism.

9.1 The Four Extension Points Compared

KindFormWho triggers itWhat it can do
Skill
a reusable playbook
Markdown file The model (via SkillTool) Packages a fixed procedure or piece of domain knowledge as a capability the model can invoke on demand
Plugin
an installable package
Code package Takes effect on install Registers new tools, slash commands, hooks, and agent types
MCP Separate process / HTTP service The model (tool call) Connects tools and resources from external systems, across languages and processes
Hook
a lifecycle callback
Script / command The system, at specific moments Intercepts, modifies, or blocks at 15 lifecycle points

9.2 The Skill System

Form

A skill is a SKILL.md file with YAML metadata at the top (the industry calls it frontmatter):

~/.claude/skills/my-skill/SKILL.md --- name: my-skill description: one sentence on what this skill does and when to use it allowed-tools: Read, Bash(git:*) ← optional: restrict which tools this skill may use hooks: ... ← optional: hooks bundled with the skill --- # Skill body Detailed steps, caveats, and examples go here... It can be long; a few thousand tokens is fine.

The core mechanism: progressive disclosure

At startup └─ Scan the skills directories, parsing only each file's frontmatter (name + description, a few dozen tokens each) ★ This part is "resident": it gets re-sent every turn ↓ When the model judges a skill might be relevant └─ It calls SkillTool(name) ↓ At runtime └─ Only then is the full SKILL.md body injected into the context (possibly thousands of tokens)

Claude Code has a dedicated function to quantify the resident cost:

export function estimateSkillFrontmatterTokens(skill: Command): number

Because every skill's frontmatter is resident in the context, the fixed cost of installing 100 skills has to be measurable — otherwise users keep installing until they discover they're burning thousands of tokens per turn for nothing.

Loader implementation details

// skills/loadSkillsDir.ts
export type LoadedFrom = ...                    // which source it was loaded from
export function getSkillsPath(...)              // path to the skills directory
export function estimateSkillFrontmatterTokens(skill: Command): number
function parseHooksFromFrontmatter(...)         // parse hooks bundled with the skill
function parseSkillPaths(frontmatter): string[] | undefined
export function parseSkillFrontmatterFields(...)
export function createSkillCommand({...})       // wrap a skill as a command object
function isSkillFile(filePath: string): boolean
function transformSkillFiles(files: MarkdownFile[]): MarkdownFile[]
function buildNamespace(targetDir: string, baseDir: string): string   // namespace
function getSkillCommandName(filePath: string, baseDir: string): string
export const getSkillDirCommands = memoize(...)  // ★ result is cached
export function clearSkillCaches()

// dynamic skills: registered at runtime, not on disk
const dynamicSkillDirs  = new Set<string>()
const dynamicSkills     = new Map<string, Command>()

A few points worth noting:

  • buildNamespace — skills have namespaces. A skill at skills/git/commit/SKILL.md gets the name git:commit. Prevents skills from different sources from colliding on names.
  • memoize — the load result is cached. Scanning the skills directories means a lot of file reads, which can't happen every time. clearSkillCaches() exists alongside it for the /reload command.
  • Dynamic skills — skills can be registered at runtime without writing a file. Plugins and MCP servers can use this mechanism to provide skills.

Skills are the bridge that “turns commands into tools”

Recall the dividing line from section 0.3: tools/ is what the model can call; commands/ is what only a human can type.

Skills break that boundary — they expose a piece of “command-style” content (a fixed procedure, domain knowledge) to the model in the form of a tool. And exposing it is cheap, because only a one-line description stays resident.

9.3 The Plugin System

The utils/plugins/ directory:

FileResponsibility
pluginLoader.ts (107 KB)Discovers, loads, validates, and registers plugins
marketplaceManager.ts (91 KB)The plugin marketplace: browse, install, update
schemas.ts (57 KB)Format definition and validation for plugin manifest files

The corresponding slash commands live under commands/plugin/:

  • ManagePlugins.tsx (314 KB) — the plugin management UI
  • BrowseMarketplace.tsx (117 KB) — the marketplace browsing UI
  • PluginSettings.tsx (126 KB) — plugin settings

Note that the UI code is bigger than the logic code. That's typical of terminal UIs — drawing an interactive list in a terminal, handling keyboard navigation, and rendering a scrollbar takes far more code than the same feature on a web page.

Cache-first loading

// QueryEngine.ts
// Cache-only: headless/SDK/CCR startup must not block on network for
// ref-tracked plugins. CCR populates the cache via CLAUDE_CODE_SYNC_PLUGIN_INSTALL
// (headlessPluginInstall) or CLAUDE_CODE_PLUGIN_SEED_DIR before this runs;
// SDK callers that need fresh source can call /reload-plugins.
const [skills, { enabled: enabledPlugins }] = await Promise.all([
  getSlashCommandToolSkills(getCwd()),
  loadAllPluginsCacheOnly(),        // ★ reads the cache only, no network requests
])

Put plainly: cache-only mode: headless / SDK / remote-environment startup must not block on the network to fetch “ref-tracked plugins.” … Callers that need fresh source can run the /reload-plugins command.

This is an important startup-performance constraint: startup in any automated scenario must not depend on the network. The network may be slow, down, or require authentication — and a job running in a pipeline can't hang because of it.

9.4 The MCP Client

MCP stands for Model Context Protocol, an open standard for connecting agents to external tool services. The services/mcp/ directory implements the client.

Two transports

TransportDescription
Standard I/O
stdio
Claude Code launches a child process and talks to it over its standard input and output. Suited to local tools
HTTPConnects to a network service. Suited to remote services and services that need authentication

Tool-name prefixes

MCP tool names get a prefix: mcp__server__tool. That way:

  • Same-named tools from different servers don't collide
  • Permission rules can be configured in bulk by server prefix (mcp__github matches every tool from that server)

But there's an exception mode: the environment variable CLAUDE_AGENT_SDK_MCP_NO_PREFIX turns the prefix off. So the Tool interface has a dedicated field to cope:

/**
 * For MCP tools: the server and tool names as received from the MCP server
 * (unnormalized). Present on all MCP tools regardless of whether `name` is
 * prefixed (mcp__server__tool) or unprefixed (CLAUDE_AGENT_SDK_MCP_NO_PREFIX mode).
 */
mcpInfo?: { serverName: string; toolName: string }

Whether or not the name is prefixed, the original server name and tool name are stored separately. That way permission checks, telemetry, and error messages all get accurate provenance without parsing the name string.

Asking the user for information (Elicitation)

The MCP protocol lets the server turn around and ask the user for information (say, “please enter your API key”). Claude Code has dedicated handling:

/**
 * Optional handler for URL elicitations triggered by tool call errors (-32042).
 * In print/SDK mode, this delegates to structuredIO.handleElicitation.
 * In REPL mode, this is undefined and the queue-based UI path is used.
 */
handleElicitation?: (
  serverName: string,
  params: ElicitRequestURLParams,
  signal: AbortSignal,
) => Promise<ElicitResult>

Two paths: in interactive mode, go through the UI queue and show a dialog (the corresponding component, ElicitationDialog.tsx, is 175 KB); in headless mode, go through the structured I/O protocol and forward the request to the outer caller.

And -32042 is a specific error code in the MCP protocol meaning “I need information from the user before I can continue.”

Other MCP-related tools

  • ListMcpResourcesTool / ReadMcpResourceTool — besides tools, MCP can provide “resources” (readable data); these two tools let the model access them
  • McpAuthTool — handles the OAuth authentication flow
  • ReadMcpResourceDirTool — lists a resource directory (for servers that declare support)

9.5 Hooks: 15 Kinds of Lifecycle Events

Hooks let users run their own scripts at specific moments in the system. This is the most powerful and most dangerous extension point — because hooks can block operations and modify arguments.

// event types in types/hooks.ts
hookEventName: z.literal('PreToolUse')          // before a tool runs
hookEventName: z.literal('PostToolUse')         // after a tool runs
hookEventName: z.literal('PostToolUseFailure')  // after a tool fails
hookEventName: z.literal('PermissionRequest')   // on a permission request
hookEventName: z.literal('PermissionDenied')    // on a permission denial
hookEventName: z.literal('UserPromptSubmit')    // when the user submits a prompt
hookEventName: z.literal('SessionStart')        // session start
hookEventName: z.literal('Setup')               // initialization / maintenance
hookEventName: z.literal('SubagentStart')       // subagent start
hookEventName: z.literal('Notification')        // notification
hookEventName: z.literal('Elicitation')         // MCP elicitation
hookEventName: z.literal('ElicitationResult')   // elicitation result
hookEventName: z.literal('CwdChanged')          // working directory changed
hookEventName: z.literal('FileChanged')         // file modified externally
hookEventName: z.literal('WorktreeCreate')      // worktree created

claude-code/src/types/hooks.ts

A few more are defined elsewhere: Stop (before finishing), PreCompact (before compaction), and PostSampling (after model sampling).

The hook execution engine

The utils/hooks/ directory:

FileResponsibility
execAgentHook.tsRuns “agent-type” hooks — the hook itself is a model call
execHttpHook.tsRuns HTTP hooks — POSTs the event to a URL
execPromptHook.tsRuns prompt hooks
ssrfGuard.tsServer-side request forgery protection — keeps HTTP hooks from being tricked into hitting internal network addresses
AsyncHookRegistry.tsRegistry of async hooks
hookEvents.tsThe event stream of hook execution (start / progress / response)
hooksConfigManager.ts / hooksConfigSnapshot.tsConfig management and snapshots
registerSkillHooks.ts / registerFrontmatterHooks.tsRegisters hooks bundled with skills
fileChangedWatcher.tsFile-change watching
skillImprovement.tsSkill self-improvement

The existence of ssrfGuard.ts deserves attention. HTTP hooks send event content to a user-configured URL. Without protection, a malicious (or manipulated) config could make Claude Code hit http://169.254.169.254/ (the cloud provider's metadata endpoint, which hands out temporary credentials) — the classic server-side request forgery attack.

Hook progress feedback

export function startHookProgressInterval(params: {...}): ...
export const HOOK_TIMING_DISPLAY_THRESHOLD_MS = 500

Hooks are scripts users wrote themselves, so their run time is completely uncontrolled. Therefore:

  • The timer is only shown past 500 milliseconds (avoids UI flicker for fast hooks)
  • An interval timer periodically emits progress events so the user knows “the system isn't stuck; your hook is running”

Conditional hook matching

Hook configs can carry conditions, such as “only fire when the Bash tool runs a git command.” That takes cooperation from the tool:

/**
 * Prepare a matcher for hook `if` conditions (permission-rule patterns like
 * "git *" from "Bash(git *)"). Called once per hook-input pair; any
 * expensive parsing happens here. Returns a closure that is called per
 * hook pattern. If not implemented, only tool-name-level matching works.
 */
preparePermissionMatcher?(input: z.infer<Input>): Promise<(pattern: string) => boolean>

Note the design: it returns a closure rather than doing the match directly. Because a single tool call may need checking against dozens of hook patterns, and parsing (say, turning a shell command into a syntax tree) is expensive. So do the “expensive preparation” once and return a “cheap matcher” to call repeatedly.

9.6 Output Styles

outputStyles/ is a small but interesting extension point: it lets users replace the “personality” portion of the system prompt.

One of its effects in the code is that the query-source identifier becomes dynamic:

// Prefix-match because promptCategory.ts sets the querySource to
// 'repl_main_thread:outputStyle:<style>' when a non-default output style
// is active. The bare 'repl_main_thread' is only used for the default style.
function isMainThreadSource(querySource: QuerySource | undefined): boolean {
  return !querySource || querySource.startsWith('repl_main_thread')
}

The comment also mentions a bug this produced:

“query.ts:350/1451 use the same startsWith pattern; the pre-existing cached-MC === 'repl_main_thread' check was a latent bug — users with a non-default output style were silently excluded from cached MC.”

Put plainly: …the pre-existing “exactly equals repl_main_thread” check in cached microcompaction was a latent bug — users with a non-default output style were silently excluded from cached microcompaction.

This is a classic “feature-interaction bug”: the output-style feature changed the format of an identifier string, while a completely unrelated feature (cached microcompaction) happened to check that string with an exact match. No error, no alert — that slice of users just quietly lost an optimization.