全文目录Contents
- 0 · The Project at a Glance, and a Map of the Code
- 1 · The Entry Layer and Startup
- 2 · The Session Layer: QueryEngine
- 2.1 The problem it solves
- 2.2 What state it holds
- 2.3 The full flow of one submitMessage
- 2.4 Why the user message must hit disk first
- 2.5 Consuming the main loop’s output: one big switch
- 2.6 The compact boundary: proactively releasing memory
- 2.7 Three kinds of exit result
- 2.8 ask(): a convenience wrapper for one-shot calls
- 3 · The Agent Main Loop ★
- 3.1 The skeleton of the loop
- 3.2 State: centralizing cross-iteration state
- 3.3 transition: a field that exists purely for testability
- 3.4 The seven transition edges, one by one
- 3.5 The error-withholding mechanism
- 3.6 Interrupt handling
- 3.7 Model fallback: three actions
- 3.8 The three laws of thinking blocks
- 3.9 Other mechanisms in the loop
- 3.10 Every exit point of the loop
- 4 · The Tool Model
- 4.1 The Tool interface: seven orthogonal capability groups
- 4.2 Why the “safety predicates” deserve their own group
- 4.3 Fail-safe defaults
- 4.4 The 40 built-in tools, by category
- 4.5 Progressive tool loading
- 4.6 Tool list assembly: a hidden constraint about caching
- 4.7 backfillObservableInput: an extreme example of cache protection
- 5 · Tool Execution
- 5.1 The execution pipeline at a glance
- 5.2 Concurrency partitioning: a greedy algorithm
- 5.3 Context modifications are queued until the batch ends
- 5.4 A single execution: the full flow of runToolUse
- 5.5 The streaming tool executor
- 5.6 The sibling abort controller: the most elegant design in the file
- 5.7 The discard mechanism
- 5.8 “Tombstone” messages
- 5.9 Final processing of results
- 6 · Context Management ★
- 7 · The Permission System
- 8 · Subagents
- 9 · The Extension System
- 10 · The Terminal UI Layer
- 10.1 Writing a Terminal UI in React
- 10.2 The Four Biggest Components
- 10.3 Why the Input Box Is 347 KB
- 10.4 The Virtualized Message List
- 10.5 Six Rendering States for Tool Results
- 10.6 Collapsing: Avoiding Screen Flood
- 10.7 87 State-Management Units
- 10.8 The Interface Between UI and Kernel: Callbacks in ToolUseContext
- 10.9 A Fun Detail: ANSI to PNG
- 11 · Persistence and Resume
- 12 · The Observability System
- 13 · Build and Distribution
9 · 扩展体系
「扩展点」的意思是:让第三方或用户自己,在不修改主程序源代码的前提下,往系统里添加能力。Claude Code 有四类扩展点,机制各不相同。
9.1 四类扩展点对照
| 类型 | 形态 | 谁触发 | 能做什么 |
|---|---|---|---|
| 技能 Skill |
Markdown 文件 | 模型(通过 SkillTool) | 把一段固定的操作流程或专业知识,做成模型可以按需调用的能力 |
| 插件 Plugin |
代码包 | 安装即生效 | 注册新的工具、斜杠命令、钩子、智能体类型 |
| MCP | 独立进程 / HTTP 服务 | 模型(工具调用) | 接入外部系统的工具和资源,跨语言、跨进程 |
| 钩子 Hook |
脚本 / 命令 | 系统在特定时机 | 在 15 个生命周期节点上拦截、修改、阻断 |
9.2 技能系统
形态
一个技能就是一个 SKILL.md 文件,头部有 YAML 元数据(业内叫 frontmatter):
核心机制:渐进式披露
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
| Kind | Form | Who triggers it | What 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):
The core mechanism: progressive disclosure
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 atskills/git/commit/SKILL.mdgets the namegit: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/reloadcommand.- 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:
| File | Responsibility |
|---|---|
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 UIBrowseMarketplace.tsx(117 KB) — the marketplace browsing UIPluginSettings.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
| Transport | Description |
|---|---|
| Standard I/O stdio | Claude Code launches a child process and talks to it over its standard input and output. Suited to local tools |
| HTTP | Connects 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__githubmatches 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 themMcpAuthTool— handles the OAuth authentication flowReadMcpResourceDirTool— 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:
| File | Responsibility |
|---|---|
execAgentHook.ts | Runs “agent-type” hooks — the hook itself is a model call |
execHttpHook.ts | Runs HTTP hooks — POSTs the event to a URL |
execPromptHook.ts | Runs prompt hooks |
ssrfGuard.ts | Server-side request forgery protection — keeps HTTP hooks from being tricked into hitting internal network addresses |
AsyncHookRegistry.ts | Registry of async hooks |
hookEvents.ts | The event stream of hook execution (start / progress / response) |
hooksConfigManager.ts / hooksConfigSnapshot.ts | Config management and snapshots |
registerSkillHooks.ts / registerFrontmatterHooks.ts | Registers hooks bundled with skills |
fileChangedWatcher.ts | File-change watching |
skillImprovement.ts | Skill 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.