(学习)Claude Code 源码架构深度解析
免责声明:本文仅供技术交流与学习使用。如有任何问题,请通过私信联系,将第一时间处理。文中涉及的商标及版权均归属于 Anthropic 公司所有。
一、项目概览

总结 Claude Code 的 8 条设计原则:
1. 启动性能是一等公民
- 快速路径 + 动态 import + 并行预取 + 编译时 DCE
- profileCheckpoint() 在每个分支点标记时间
- MDM + Keychain 读取在 import 副作用中并行启动
2. 安全性纵深防御
- 6 层权限检查 + Hook 拦截 + ML 分类器
- 反调试机制(外部构建检测 Inspector 直接退出)
- UNC 路径拦截防止 NTLM 凭据泄漏
- 设备文件黑名单防止无限读取
- 原子写入防止竞态条件
3. 流式一切
- AsyncGenerator 贯穿整个消息流
- 工具执行流式上报 progress
- 历史读取用异步生成器反向遍历
- REPL 渲染异步于查询执行
4. 状态管理极简
- 34 行自研 Store,Object.is() 变更检测
- 不引入任何第三方状态库
- useSyncExternalStore 对接 React
5. 可扩展优先
- 43 个工具通过统一接口注册
- MCP 让外部工具无缝接入(5 种传输协议)
- Hook 系统让用户自定义行为(4 种执行方式)
- 技能系统让社区贡献能力
6. 渐进式发布
- 20+ 个 Feature Gate,编译时消除未启用代码
- GrowthBook 运行时灰度
- USER_TYPE 区分内部/外部构建
7. 上下文即生命线
- Memoized 系统上下文(一次对话只计算一次)
- 三种压缩策略应对上下文窗口限制
- 文件读取 LRU 缓存 + 去重
- 工具结果预算控制
- ToolSearch 延迟加载省 token
8. 测试友好
-
依赖注入贯穿核心模块
-
QueryDeps 允许注入 mock
-
Feature Gate 在 bun test 下返回 false
-
TestingPermissionTool 仅测试环境启用
二、整体架构设计

详细流程

三、入口层:毫秒级的启动优化
3.1 快速路径(Fast Path)
cli.tsx 是整个应用的入口,只有 302 行,但信息密度极高。
// cli.tsx — 第一行就是 feature flag
import { feature } from 'bun:bundle';
// 顶层副作用:环境准备
process.env.COREPACK_ENABLE_AUTO_PIN = '0'; // 防止 corepack 污染 package.json
// CCR 远程环境设置 8GB 堆上限
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
process.env.NODE_OPTIONS = existing
? `${existing} --max-old-space-size=8192`
: '--max-old-space-size=8192';
}
关键设计:所有 import 都是动态的。–version 路径零模块加载:
async function main(): Promise<void> {
const args = process.argv.slice(2);
// 快速路径 1: --version — 零 import,直接输出
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
console.log(`${MACRO.VERSION} (Claude Code)`); // 编译时内联
return;
}
// 快速路径 2: --dump-system-prompt (Ant-only, 编译时消除)
if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') { ... }
// 快速路径 3: Chrome 原生宿主
if (process.argv[2] === '--chrome-native-host') { ... }
// 快速路径 4: Daemon worker (内部 supervisor 用)
if (feature('DAEMON') && args[0] === '--daemon-worker') { ... }
// 快速路径 5: Bridge 远程控制
if (feature('BRIDGE_MODE') && (args[0] === 'remote-control' || args[0] === 'rc')) { ... }
}
每个分支都有 profileCheckpoint() 调用,方便 Anthropic 内部做启动性能分析。
3.2 反调试机制
外部构建包含一个反调试检查,检测到 --inspect 或 Node Inspector 活跃时直接退出:
// 外部构建检测调试器
if ("external" !== 'ant' && isBeingDebugged()) {
process.exit(1); // 静默退出,无错误信息
}
这里 “external” 是编译时替换的字符串——Anthropic 内部构建替换为 ‘ant’,外部构建保持 ‘external’。所以只有外部用户会被拦截,内部开发者不受影响。
3.3 main.tsx:4683 行的超级编排器
它的前 20 行就包含了三个并行的副作用:
// 第 1 行:标记入口时间戳
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');
// 第 2 行:火速启动 MDM 子进程(macOS plutil / Windows reg query)
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();
// 第 3 行:并行读取 macOS Keychain(OAuth token + 旧版 API key)
import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();
这三个操作在模块加载阶段(import 副作用)就开始执行,比 main() 函数早 135ms 以上。这是因为后续 135ms 花在加载其余 100+ 个 import 上——这段时间内 MDM 和 Keychain 的子进程已经在后台跑完了。
3.4 迁移系统
main.tsx 包含一个版本化的迁移系统,当前版本号是 11:
const CURRENT_MIGRATION_VERSION = 11;
function runMigrations(): void {
if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
migrateAutoUpdatesToSettings();
migrateSonnet45ToSonnet46(); // 模型名升级
migrateOpusToOpus1m(); // Opus → Opus 1M
migrateBypassPermissionsAcceptedToSettings(); // 权限状态迁移
// ... 更多迁移
saveGlobalConfig(prev => ({
...prev,
migrationVersion: CURRENT_MIGRATION_VERSION
}));
}
}
每次版本号变更时,所有迁移函数幂等执行——已经迁移过的会 early return。
3.5 REPL 启动
最终通过 launchRepl() 启动交互界面:
// replLauncher.tsx — 动态 import App 和 REPL
export async function launchRepl(root, appProps, replProps, renderAndRun) {
const { App } = await import('./components/App.js');
const { REPL } = await import('./screens/REPL.js');
await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>);
}
App 组件包裹了 ThemeProvider(在 ink.ts 中注入),提供亮/暗主题支持。Ink 本身是主题无关的——Claude Code 通过全局包装层注入主题。
四、状态管理:34 行的极简 Store
整个应用的状态管理只有 34 行代码,不依赖任何第三方库:
// state/store.ts — 完整源码
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void
export type Store<T> = {
getState: () => T
setState: (updater: (prev: T) => T) => void
subscribe: (listener: Listener) => () => void
}
export function createStore<T>(
initialState: T,
onChange?: OnChange<T>,
): Store<T> {
let state = initialState
const listeners = new Set<Listener>()
return {
getState: () => state,
setState: (updater: (prev: T) => T) => {
const prev = state
const next = updater(prev)
if (Object.is(next, prev)) return // 核心:引用相等检查
state = next
onChange?.({ newState: next, oldState: prev })
for (const listener of listeners) listener()
},
subscribe: (listener: Listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
}
}
配合 React 18 的 useSyncExternalStore 实现最小化重渲染:
// state/AppState.tsx
export function useAppState<T>(selector: (state: AppState) => T): T {
const store = useAppStore();
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
);
}
为什么不用 Zustand 或 Redux? 因为 Claude Code 的状态更新模式非常简单——没有中间件、没有 devtools、没有异步 action。一个 Object.is() + Set 就够了。34 行代码 vs Zustand 的 1000+ 行,性能更好,依赖更少。
AppState 的形状
export type AppState = DeepImmutable<{
settings: SettingsJson
mainLoopModel: ModelSetting
toolPermissionContext: ToolPermissionContext
verbose: boolean
statusLineText: string | undefined
isBriefOnly: boolean
spinnerTip?: string
kairosEnabled: boolean
// Bridge/Remote 状态
remoteSessionUrl: string | undefined
remoteConnectionStatus: 'connecting' | 'connected' | 'reconnecting' | 'disconnected'
replBridgeEnabled: boolean
replBridgeConnected: boolean
// ...
}> & {
// 可变部分
tasks: { [taskId: string]: TaskState }
agentNameRegistry: Map<string, AgentId>
foregroundedTaskId?: string
mcp: {
clients: MCPServerConnection[]
tools: Tool[]
commands: Command[]
resources: Record<string, ServerResource[]>
}
}
注意:大部分字段是 DeepImmutable(只读递归类型),但 tasks 和 mcp 是可变的——因为它们需要高频更新。
五、查询引擎:AsyncGenerator 驱动的对话循环
5.1 QueryEngine 类
QueryEngine 管理一次对话的完整生命周期,核心是一个异步生成器:
export type QueryEngineConfig = {
cwd: string
tools: Tools
commands: Command[]
mcpClients: MCPServerConnection[]
agents: AgentDefinition[]
canUseTool: CanUseToolFn
getAppState: () => AppState
setAppState: (f: (prev: AppState) => AppState) => void
readFileCache: FileStateCache
customSystemPrompt?: string
appendSystemPrompt?: string
thinkingConfig?: ThinkingConfig
maxTurns?: number // SDK 模式下限制轮数
maxBudgetUsd?: number // 成本预算
// ...
}
export class QueryEngine {
private mutableMessages: Message[] = [] // 跨 turn 持久化
private fileStateCache: FileStateCache // 文件读取缓存
private totalUsage: NonNullableUsage // 累计 token
private permissionDenials: SDKPermissionDenial[] = []
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean }
): AsyncGenerator<SDKMessage, void, unknown> {
// 逐条 yield 消息,实现真正的流式响应
}
}
5.2 数据流详解
用户输入 "帮我写一个 TODO 应用"
│
▼
processUserInput()
│ 解析斜杠命令 (/commit, /review 等)
│ 展开粘贴内容 ([Pasted text #1 +10 lines])
│ 处理附件(图片、PDF)
▼
fetchSystemPromptParts()
│ ① getSystemContext() — Git状态(memoized, 一次对话只算一次)
│ ├─ getBranch()
│ ├─ getDefaultBranch()
│ ├─ git status --short (截断至2000字符)
│ ├─ git log --oneline -n 5
│ └─ git config user.name
│ ② getUserContext() — claude.md 发现 + 当前日期
│ ③ getCoordinatorUserContext() — Coordinator 模式专用
│ ④ 技能<a href="https://www.mianshiya.com/bank/1860931478862618626" class="keyword-highlight" target="_blank" rel="noopener noreferrer">前端</a>matter + 工具描述
▼
query() — 核心查询函数 (1729行)
│
├─ normalizeMessagesForAPI()
│ 过滤掉 progress/system 消息
│ 将内部消息格式转为 API 格式
│
├─ queryModelWithStreaming()
│ 调用 Claude API (带 prompt cache)
│ yield AssistantMessage (流式)
│
├─ 提取 tool_use blocks
│
├─ StreamingToolExecutor.execute()
│ ├─ 并发安全工具 → Promise.all() 并行执行
│ └─ 非安全工具 → 顺序执行
│
├─ applyToolResultBudget()
│ 大结果截断,防止上下文膨胀
│
├─ executePostSamplingHooks()
│ Stop hook / PostToolUse hook
│
├─ calculateTokenWarningState()
│ 检查是否接近上下文窗口上限
│
└─ autoCompact / snipCompact (如果启用)
压缩旧消息,释放上下文空间
5.3 Agent 主循环
// query.ts
async function* queryLoop(params: QueryParams, consumedCommandUuids: string[]) {
let state: State = {
messages: params.messages,
toolUseContext: params.toolUseContext,
autoCompactTracking: undefined,
maxOutputTokensRecoveryCount: 0,
hasAttemptedReactiveCompact: false,
turnCount: 1,
// ...
}
// eslint-disable-next-line no-constant-condition
while (true) {
// 1. 上下文压缩(防止 token 爆炸)
// 2. 调用大模型,流式获取响应
// 3. 解析模型返回的工具调用
// 4. 执行工具,获取结果
// 5. 把结果追加到对话历史
// 6. 如果没有新的工具调用,结束;否则继续循环
}
}
这在 AI 领域叫 ReAct 机制(推理 + 执行),让 AI 自己形成「思考 → 行动 → 观察 → 再思考」的闭环。Claude Code 的源码就是这个理论最接地气的工程实现了。
六、工具系统:43 个模块的插件式架构
6.1 工具注册
tools.ts 是工具的注册中心,390 行代码管理所有工具的生命周期:
// tools.ts — 所有工具的注册
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
// Ant 内部构建嵌入了 bfs/ugrep,不需要独立的 Glob/Grep
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
FileReadTool,
FileEditTool,
FileWriteTool,
NotebookEditTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
SkillTool,
// ... 更多工具
// Feature-gated 工具 (编译时消除)
...(WebBrowserTool ? [WebBrowserTool] : []),
...(SleepTool ? [SleepTool] : []),
...(SnipTool ? [SnipTool] : []),
...(WorkflowTool ? [WorkflowTool] : []),
// 仅在 Ant 内部构建启用
...(process.env.USER_TYPE === 'ant' ? [ConfigTool, TungstenTool] : []),
...(REPLTool ? [REPLTool] : []),
// 仅在测试环境
...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
]
}
6.2 工具池组装
内置工具和 MCP 工具通过 assembleToolPool() 合并:
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
// 关键设计:按 name 排序,保证 prompt cache 稳定性
// 如果 MCP 工具插入到内置工具之间,会导致所有下游 cache key 失效
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
// uniqBy 保留插入顺序 → 内置工具同名时优先
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
6.3 Simple 模式
当 CLAUDE_CODE_SIMPLE=1 时,工具池缩减到最小:
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
// Coordinator 模式额外加入 Agent + TaskStop
if (coordinatorModeModule?.isCoordinatorMode()) {
simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
}
return filterToolsByDenyRules(simpleTools, permissionContext)
}
6.4 工具接口
每个工具实现统一的 Tool 接口(Tool.ts, 792 行):
type Tool<Input, Output> = {
name: string
aliases?: string[] // 别名
searchHint?: string // ToolSearch 匹配用
shouldDefer?: boolean // 是否延迟加载(省token)
alwaysLoad?: boolean // 是否总是加载
// 核心
call(args, context, canUseTool, parentMessage, onProgress?)
description(): Promise<string>
prompt(): Promise<string> // 给模型看的使用说明
inputJSONSchema: ToolInputJSONSchema // Zod → JSON Schema
// 权限
checkPermissions(): Promise<PermissionResult>
validateInput(): Promise<ValidationResult>
// 属性
isConcurrencySafe(): boolean // 能否并行
isReadOnly(): boolean // 只读?
isDestructive(): boolean // 破坏性?
// UI 渲染
renderToolUseMessage()
renderToolResultMessage()
renderToolUseProgressMessage()
}
6.5 BashTool 实现细节
// BashTool 输入 Schema
const fullInputSchema = z.strictObject({
command: z.string(),
timeout: z.number().optional(),
description: z.string().optional(),
run_in_background: z.boolean().optional(),
dangerouslyDisableSandbox: z.boolean().optional(),
_simulatedSedEdit: z.object({ // 隐藏字段:sed 编辑模拟
filePath: z.string(),
newContent: z.string()
}).optional()
})
// 命令分类(用于 UI 折叠展示)
const BASH_SEARCH_COMMANDS = new Set([
'find', 'grep', 'rg', 'ag', 'ack', 'locate', 'which', 'whereis'
])
const BASH_READ_COMMANDS = new Set([
'cat', 'head', 'tail', 'less', 'more', 'wc', 'stat', 'file', 'jq', 'awk'
])
const BASH_SILENT_COMMANDS = new Set([
'mv', 'cp', 'rm', 'mkdir', 'rmdir', 'chmod', 'chown', 'touch', 'ln', 'cd'
])
6.6 FileEditTool 的原子写入
文件编辑实现了无 async 间隙的原子写入,防止竞态条件:
// 关键:staleness 检查和写入之间不能有 async 操作
const lastWriteTime = getFileModificationTime(absoluteFilePath)
if (lastWriteTime > readTimestamp.timestamp) {
// 文件在读取后被外部修改了
throw new Error(FILE_UNEXPECTEDLY_MODIFIED_ERROR)
}
// 同步读取 + 同步写入,中间无 async yield
const { content, encoding, lineEndings } = readFileForEdit(absoluteFilePath)
const { patch, updatedFile } = getPatchForEdit({
filePath, originalFileContents: content,
oldString: actualOldString, newString, replaceAll
})
writeTextContent(absoluteFilePath, updatedFile, encoding, lineEndings) // 原子写入
// 立即更新 readFileState 缓存
readFileState.set(absoluteFilePath, {
content: updatedFile,
timestamp: getFileModificationTime(absoluteFilePath),
})
6.7 FileReadTool 的安全防护
// 设备文件黑名单 — 防止读取无限流
const BLOCKED_DEVICE_PATHS = new Set([
'/dev/zero', '/dev/random', '/dev/urandom',
'/dev/stdin', '/dev/tty', '/dev/console',
])
// UNC 路径拦截 — 防止 NTLM 凭据泄漏
if (fullFilePath.startsWith('\\\\') || fullFilePath.startsWith('//')) {
return { result: true } // 静默跳过
}
// 文件读取去重 — 同一文件同一范围不重复读取
const existingState = readFileState.get(fullFilePath)
if (existingState && existingState.offset === offset && existingState.limit === limit) {
const mtimeMs = await getFileModificationTimeAsync(fullFilePath)
if (mtimeMs === existingState.timestamp) {
return { data: { type: 'file_unchanged' } } // 返回stub,不重发内容
}
}
6.8 GrepTool 的 Ripgrep 集成
// VCS 目录排除
const VCS_DIRECTORIES_TO_EXCLUDE = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl']
for (const dir of VCS_DIRECTORIES_TO_EXCLUDE) {
args.push('--glob', `!${dir}`)
}
args.push('--max-columns', '500') // 防止 base64/minified 内容膨胀
// 默认分页限制 — 防止上下文膨胀
const DEFAULT_HEAD_LIMIT = 250
// 负数 pattern 需要 -e 前缀,否则 rg 当作 flag 解析
if (pattern.startsWith('-')) {
args.push('-e', pattern)
}
6.9 ToolSearchTool:延迟加载机制
当工具总数超过阈值时,部分工具只发送 name,不发送完整 schema:
// ToolSearch 的 select 语法
// "select:ToolA,ToolB" — 精确选择
const selectMatch = query.match(/^select:(.+)$/i)
// 关键词搜索评分
if (parsed.parts.includes(term)) {
score += parsed.isMcp ? 12 : 10 // MCP 工具名额外加权
} else if (parsed.parts.some(part => part.includes(term))) {
score += parsed.isMcp ? 6 : 5
}
if (pattern.test(hintNormalized)) {
score += 4 // searchHint 匹配
}
if (pattern.test(descNormalized)) {
score += 2 // 描述匹配
}
6.10 并发执行策略
class StreamingToolExecutor {
async execute(toolUses, context) {
// Read/Grep/Glob 标记为 concurrencySafe → Promise.all() 并行
const safe = toolUses.filter(t => findTool(t).isConcurrencySafe())
const unsafe = toolUses.filter(t => !findTool(t).isConcurrencySafe())
const safeResults = await Promise.all(
safe.map(t => this.executeSingle(t, context))
)
// Bash/Write/Edit 等有副作用的工具 → 严格串行
const unsafeResults = []
for (const t of unsafe) {
unsafeResults.push(await this.executeSingle(t, context))
}
}
}
七、上下文管理:Memoized
7.1 内容检索策略
在负责记忆系统的 memdir/memdir.ts 文件里有个 buildSearchingPastContextSection 函数,写得很明确,用最朴素的 Grep 文本搜索。
// memdir/memdir.ts — 记忆搜索指令
const memSearch = `grep -rn "<search term>" ${autoMemDir} --include="*.md"`
const transcriptSearch = `grep -rn "<search term>" ${projectDir}/ --include="*.jsonl"`
7.2 三层记忆架构
第一层、MEMORY.md(热数据,常驻加载)
MEMORY.md 就像一本书的目录,每次对话都会加载到上下文里。
限制文件大小:
// memdir/memdir.ts — 记忆索引文件
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
// ~125 chars/line at 200 lines. At p97 today; catches long-line indexes that
// slip past the line cap (p100 observed: 197KB under 200 lines).
export const MAX_ENTRYPOINT_BYTES = 25_000
截断逻辑:
// memdir/memdir.ts — 截断逻辑
export function truncateEntrypointContent(raw: string): EntrypointTruncation {
// 先按行数截断
let truncated = wasLineTruncated
? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')
: trimmed
// 再按字节数截断,在最后一个换行符处切,避免截断到一半
if (truncated.length > MAX_ENTRYPOINT_BYTES) {
const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)
truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES)
}
// 截断后追加 WARNING,让 AI 知道这个文件没加载完
return {
content: truncated +
`\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded.`,
}
}
第二层、话题文件(温数据,按需加载)
编码偏好、项目的架构约定、之前踩过的坑这些细节信息,都存在单独的话题文件里,比如 user_role.md、feedback_testing.md。新对话开始时,Claude Code 会用 Sonnet 小模型来挑选最多 5 个跟当前对话相关的文件加载进来。在负责记忆召回的 findRelevantMemories.ts 文件中,可以看到它挑选记忆的提示词:
// memdir/findRelevantMemories.ts — 记忆召回
const SELECT_MEMORIES_SYSTEM_PROMPT = `You are selecting memories that will be
useful to Claude Code as it processes a user's query.
Return a list of filenames for the memories that will clearly be useful (up to 5).
- If a list of recently-used tools is provided, do not select memories that are
usage reference or API documentation for those tools (Claude Code is already
exercising them). DO still select memories containing warnings, gotchas, or
known issues about those tools — active use is exactly when those matter.`
第三层、历史对话(冷数据,Grep 搜索)
更早之前的历史对话会被存成 .jsonl 格式的文件,需要的时候用 Grep 搜关键词就能找到。
总结一下,不同温度的数据用不同方式管理。热的常驻、温的按需、冷的搜索。
7.3 上下文压缩策略
当对话接近 token 上限时,有三种压缩策略(通过 Feature Gate 控制):
// services/compact/autoCompact.ts
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000 // 触发阈值缓冲
export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // 警告阈值
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
// 有效上下文窗口 = 总窗口 - 预留输出空间(20000)
export function getEffectiveContextWindowSize(model: string): number {
const reservedTokensForSummary = Math.min(
getMaxOutputTokensForModel(model),
20_000
)
return contextWindow - reservedTokensForSummary
}
三种策略:
八、权限系统:多层纵深防御

8.1 权限模式
const PERMISSION_MODE_CONFIG = {
default: { title: 'Default' }, // 交互式提示
plan: { title: 'Plan Mode' }, // 只读+规划,不执行
acceptEdits: { title: 'Accept edits' }, // 自动接受编辑
bypassPermissions: { title: 'Bypass' }, // 全部放行
dontAsk: { external: 'dontAsk' }, // 不提示
auto: { title: 'Auto mode' } // ML 分类器自动判断
}
8.2 完整检查链
用户提交工具调用(如 Bash: "rm -rf /")
│
▼
① validateInput() — 工具自定义校验
│ 例:FileEditTool 检查文件大小是否超过 1GiB
▼
② alwaysDenyRules 检查 — 黑名单直接拦截
│ 匹配: 工具名 + ruleContent (glob pattern)
│ 例:Deny "Bash(rm -rf *)"
▼
③ alwaysAllowRules 检查 — 白名单直接放行
│ 例:Allow "Bash(git *)"
▼
④ Auto Mode Classifier (Feature-gated: TRANSCRIPT_CLASSIFIER)
│ ML 模型分析整个对话上下文
│ 判断操作是否安全
│ ├→ 安全 → 放行
│ ├→ 危险 → 拒绝
│ └→ 不确定 → 继续到 ⑤
▼
⑤ PreToolUse Hook 检查
│ 执行用户定义的 <a href="https://www.mianshiya.com/bank/1812067519566053377" class="keyword-highlight" target="_blank" rel="noopener noreferrer">Shell</a> 脚本
│ exitCode=0 → 放行
│ exitCode=2 → 拦截
▼
⑥ 交互式提示 — 弹出终端 UI 询问用户
│ Allow / Deny / Always Allow
▼
canUseTool() — 最终裁决
8.3 拒绝追踪与降级
// 连续被拒绝 3 次后,从自动拒绝降级为弹窗询问
// 防止 AI 陷入 "尝试 → 被拒 → 再尝试" 的死循环
permissionDenials.push({ tool, input, reason })
if (denialCount >= 3) {
fallbackToPrompt = true // 弹窗让用户手动决定
}
8.4 危险路径检测
// utils/permissions/filesystem.ts (62KB)
export const DANGEROUS_FILES = [
'.gitconfig', '.bashrc', '.zshrc', '.mcp.json', '.claude.json'
]
export const DANGEROUS_DIRECTORIES = [
'.git', '.vscode', '.idea', '.claude'
]
// bypassPermissions 模式下,检测 root 权限 + 沙箱状态
if (process.getuid() === 0 && process.env.IS_SANDBOX !== '1') {
console.error('--dangerously-skip-permissions cannot be used with root/sudo')
process.exit(1)
}
九、成本追踪:每模型粒度的会话管理
// cost-tracker.ts
type StoredCostState = {
totalCostUSD: number
totalAPIDuration: number
totalAPIDurationWithoutRetries: number
totalToolDuration: number
totalLinesAdded: number
totalLinesRemoved: number
modelUsage: { [modelName: string]: ModelUsage }
}
会话恢复
成本数据通过 sessionId 匹配恢复,防止不同会话的成本混淆:
export function getStoredSessionCosts(sessionId: string): StoredCostState | undefined {
const projectConfig = getCurrentProjectConfig()
// 只有 sessionId 匹配才恢复
if (projectConfig.lastSessionId !== sessionId) {
return undefined
}
return { totalCostUSD: projectConfig.lastCost ?? 0, ... }
}
Advisor 递归成本追踪
Claude Code 支持 “Advisor” 模式(另一个模型辅助当前模型),成本递归累加:
export function addToTotalSessionCost(cost, usage, model) {
// 递归追踪 advisor 用量
for (const advisorUsage of getAdvisorUsage(usage)) {
const advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage)
logEvent('tengu_advisor_tool_token_usage', {
advisor_model: advisorUsage.model,
cost_usd_micros: Math.round(advisorCost * 1_000_000),
})
totalCost += addToTotalSessionCost(advisorCost, advisorUsage, advisorUsage.model)
}
}
十、会话历史:JSONL + 反向读取
存储格式
type LogEntry = {
display: string // 显示文本
pastedContents: Record<number, StoredPastedContent> // 粘贴内容引用
timestamp: number
project: string // 项目路径
sessionId?: string
}
// 粘贴内容分流
type StoredPastedContent = {
id: number
type: 'text' | 'image'
content?: string // 小于 1024 字符 → 内联
contentHash?: string // 大于 1024 字符 → hash 引用,异步写入磁盘
}
反向读取(Up 键历史)
async function* makeLogEntryReader(): AsyncGenerator<LogEntry> {
// 1. 先 yield 未刷盘的 pending 条目
for (let i = pendingEntries.length - 1; i >= 0; i--) {
yield pendingEntries[i]!
}
// 2. 从磁盘反向读取 JSONL
for await (const line of readLinesReverse(historyPath)) {
const entry = deserializeLogEntry(line)
// 跳过已删除的条目
if (skippedTimestamps.has(entry.timestamp)) continue
yield entry
}
}
刷盘与文件锁
async function immediateFlushHistory() {
const historyPath = join(getClaudeConfigHomeDir(), 'history.jsonl')
// 文件锁(10s stale, 3 次重试)
release = await lock(historyPath, {
stale: 10000,
retries: { retries: 3, minTimeout: 50 },
})
// 批量追加
const jsonLines = pendingEntries.map(entry => jsonStringify(entry) + '\n')
pendingEntries = []
await appendFile(historyPath, jsonLines.join(''), { mode: 0o600 })
}
十一、Coordinator 模式:多智能体编排
Feature-gated 的多智能体模式(COORDINATOR_MODE):
// coordinator/coordinatorMode.ts
export function getCoordinatorSystemPrompt(): string {
return `You are Claude Code, an AI assistant that orchestrates
software engineering tasks across multiple workers.
## Your Role
You are a **coordinator**. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible
## Your Tools
- **Agent** - Spawn a new worker
- **SendMessage** - Continue an existing worker
- **TaskStop** - Stop a running worker`
}
// Worker 工具集过滤(Simple 模式下只给 Bash/Read/Edit)
export function getCoordinatorUserContext(mcpClients, scratchpadDir?) {
const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME]
: Array.from(ASYNC_AGENT_ALLOWED_TOOLS)
.filter(name => !INTERNAL_WORKER_TOOLS.has(name))
}
十二、反蒸馏和卧底模式
反蒸馏
反蒸馏就是通过录制 Claude Code 的 API 流量来「蒸馏」它的能力,就是把大模型的输入输出录下来,用来训练自己的小模型。
Anthropic 的应对策略直接往 API 请求里注入假工具定义:
// services/api/claude.ts — 负责模型 API 调用
// Anti-distillation: send fake_tools opt-in for 1P CLI only
if (feature('ANTI_DISTILLATION_CC')
&& process.env.CLAUDE_CODE_ENTRYPOINT === 'cli'
&& shouldIncludeFirstPartyOnlyBetas()
) {
result.anti_distillation = ['fake_tools']
}
卧底模式
卧底模式是Anthropic 内部员工用 Claude Code 往开源项目提交代码时,会自动启用这个模式:
// utils/undercover.ts — 负责隐藏 AI 贡献痕迹
/**
* Undercover mode — safety utilities for contributing to public repos.
* When active, Claude Code strips all attribution to avoid leaking internal
* model codenames, project names, or other Anthropic-internal information.
* The model is not told what model it is.
*
* There is NO force-OFF. This guards against model codename leaks.
*/
export function isUndercover(): boolean {
if (process.env.USER_TYPE === 'ant') {
if (isEnvTruthy(process.env.CLAUDE_CODE_UNDERCOVER)) return true
// Auto: active unless we've confirmed we're in an internal repo.
return getRepoClassCached() !== 'internal'
}
return false
}
十三、彩蛋:Buddy 宠物系统
// buddy/types.ts
export const SPECIES = [
'duck', 'goose', 'blob', 'cat', 'dragon', 'octopus', 'owl', 'penguin',
'turtle', 'snail', 'ghost', 'axolotl', 'capybara', 'cactus',
'robot', 'rabbit', 'mushroom', 'chonk',
] as const // 18 种物种
export const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const
export type CompanionBones = {
rarity: Rarity
species: Species
eye: Eye
hat: Hat
shiny: boolean // 闪光版
stats: Record<StatName, number> // Debugging, Patience, Chaos, Wisdom, Snark
}
export type CompanionSoul = {
name: string // AI 生成的名字
personality: string // AI 生成的性格
}
用 hash(userId) 确定性生成,保证同一用户在所有会话中看到同一只宠物。首次 “孵化” 时由 Claude 生成名字和性格。还有一个 CompanionSprite.tsx (45KB) 实现了帧动画。
十四、Feature Gate:编译时 vs 运行时
Claude Code 使用两层特性开关:
编译时(Bun DCE)
import { feature } from 'bun:bundle';
// 编译时完全消除 — 外部构建中这段代码不存在
if (feature('COORDINATOR_MODE')) {
const module = require('./coordinatorMode.js')
}
已发现的 Feature Gate:
Flag 说明
COORDINATOR_MODE 多智能体编排
VOICE_MODE 语音输入
HISTORY_SNIP 历史截断压缩
TRANSCRIPT_CLASSIFIER ML 权限分类器
REACTIVE_COMPACT 主动上下文压缩
CONTEXT_COLLAPSE 语义上下文折叠
KAIROS Assistant 模式
BRIDGE_MODE 远程桥接
PROACTIVE 主动提示
AGENT_TRIGGERS Cron 触发器
UDS_INBOX Unix Socket 收件箱
DAEMON 守护进程模式
ABLATION_BASELINE 消融实验基线
DUMP_SYSTEM_PROMPT 导出系统提示词
WORKFLOW_SCRIPTS 工作流脚本
WEB_BROWSER_TOOL 浏览器工具
TERMINAL_PANEL 终端面板
EXPERIMENTAL_SKILL_SEARCH 技能搜索
KAIROS_GITHUB_WEBHOOKS GitHub Webhook 订阅
CHICAGO_MCP Computer Use MCP
OVERFLOW_TEST_TOOL 溢出测试
运行时(GrowthBook)
// 基于用户/环境动态开关
const isEnabled = getFeatureValue_CACHED_MAY_BE_STALE('tengu_some_feature', false)
用户类型区分
// 编译时字符串替换
if ("external" !== 'ant' && isBeingDebugged()) {
process.exit(1) // 外部构建禁止调试
}
// 运行时环境变量
if (process.env.USER_TYPE === 'ant') {
// Anthropic 内部工具:ConfigTool, TungstenTool, REPLTool
}
十五、配额限制与"封号"机制
很多人关心:Claude Code 会不会封号?源码里有什么限制机制?
15.1 没有传统"封号",但有严格的配额执行
Claude Code 不在客户端做任何封号判断。所有限制通过 API 响应头 anthropic-ratelimit-unified-* 由服务端下发,客户端只是忠实执行。
// services/claudeAiLimits.ts
type QuotaStatus = 'allowed' | 'allowed_warning' | 'rejected'
type RateLimitType =
| 'five_hour' // 5 小时滑动窗口
| 'seven_day' // 7 天窗口
| 'seven_day_opus' // Opus 专用 7 天限制
| 'seven_day_sonnet' // Sonnet 专用 7 天限制
| 'overage' // 超额使用(付费后仍有上限)
15.2 服务端响应头解析
每次 API 调用后,客户端解析以下响应头:
function computeNewLimitsFromHeaders(headers: Headers): ClaudeAILimits {
// 核心状态:allowed / allowed_warning / rejected
const status = headers.get('anthropic-ratelimit-unified-status') as QuotaStatus || 'allowed'
// 重置时间(Unix 秒)
const resetsAt = Number(headers.get('anthropic-ratelimit-unified-reset'))
// 命中的限制类型
const rateLimitType = headers.get('anthropic-ratelimit-unified-representative-claim')
// 超额使用状态
const overageStatus = headers.get('anthropic-ratelimit-unified-overage-status')
// 超额被禁用的原因(12 种可能)
const overageDisabledReason = headers.get(
'anthropic-ratelimit-unified-overage-disabled-reason'
)
// 使用率(0~1)
const utilization5h = Number(headers.get('anthropic-ratelimit-unified-5h-utilization'))
const utilization7d = Number(headers.get('anthropic-ratelimit-unified-7d-utilization'))
}
当 status === ‘rejected’ 时,所有 API 调用立即被阻止,用户必须等到 resetsAt 时间。
15.3 超额禁用原因(12 种)
type OverageDisabledReason =
| 'overage_not_provisioned' // 未开通超额
| 'org_level_disabled' // 组织级别禁用
| 'org_level_disabled_until' // 组织级别临时禁用
| 'out_of_credits' // 余额不足
| 'seat_tier_level_disabled' // 席位等级禁用
| 'member_level_disabled' // 个人账户禁用
| 'seat_tier_zero_credit_limit' // 席位等级信用额度为零
| 'group_zero_credit_limit' // 组信用额度为零
| 'member_zero_credit_limit' // 个人信用额度为零
| 'org_service_level_disabled' // 组织服务级别禁用
| 'org_service_zero_credit_limit'// 组织服务信用额度为零
| 'no_limits_configured' // 未配置限制
| 'unknown'
15.4 预飞检查(Pre-flight)
每次会话启动时,Claude Code 发一个最小请求来探测配额状态:
async function makeTestQuery() {
const model = getSmallFastModel() // 用最小模型
const anthropic = await getAnthropicClient({
maxRetries: 0,
model,
source: 'quota_check',
})
return anthropic.beta.messages.create({
model,
max_tokens: 1, // 只请求 1 个 token
messages: [{ role: 'user', content: 'quota' }],
}).asResponse()
}
用最便宜的模型、最少的 token 做探测,只为拿到响应头中的配额信息。
15.5 早期预警系统
Claude Code 会预判你的消耗速度,在你还没被限制时就发出警告:
const EARLY_WARNING_CONFIGS = [
{
rateLimitType: 'five_hour',
windowSeconds: 5 * 60 * 60,
thresholds: [
{ utilization: 0.9, timePct: 0.72 }, // 用了90%但时间只过72%
],
},
{
rateLimitType: 'seven_day',
windowSeconds: 7 * 24 * 60 * 60,
thresholds: [
{ utilization: 0.75, timePct: 0.6 }, // 用了75%但时间只过60%
{ utilization: 0.5, timePct: 0.35 }, // 用了50%但时间只过35%
{ utilization: 0.25, timePct: 0.15 }, // 用了25%但时间只过15%
],
},
]
翻译一下:如果你在 7 天窗口的前 35% 时间里就用掉了 50% 的配额,系统会警告你正在 “燃烧” 配额。
15.6 429 错误处理
收到 HTTP 429 时,状态被强制设为 rejected:
export function extractQuotaStatusFromError(error: APIError): void {
if (error.status !== 429) return
let newLimits = { ...currentLimits }
if (error.headers) {
newLimits = computeNewLimitsFromHeaders(error.headers)
}
// 无论 headers 是否存在,429 = 立刻 rejected
newLimits.status = 'rejected'
emitStatusChange(newLimits)
}
15.7 用户看到的限制信息
// 实际展示给用户的消息
"You've hit your session limit · resets in 4 hours"
"You've hit your weekly limit"
"You've hit your Opus limit"
"You're out of extra usage"
15.8 反调试机制
外部构建中,检测到 Node.js Inspector 活跃时直接退出:
// 检测 --inspect、--inspect-brk、--debug、--debug-brk
// 以及 inspector.url() 是否活跃
if ("external" !== 'ant' && isBeingDebugged()) {
process.exit(1) // 静默退出,无任何错误信息
}
“external” 是编译时替换的字符串——Anthropic 内部构建替换为 ‘ant’,外部构建保持 ‘external’。所以只有外部用户被拦截调试。
15.9 客户端无法绕过
源码中没有任何绕过机制:
- 不能伪造响应头
- 不能修改重置时间
- 不能欺骗授权
- 所有限制执行都是服务端驱动的
- 429 错误是 fatal,不可重试(超过限制后)
结论:Claude Code 的限制完全是服务端控制的。客户端只是一个忠实的执行者,没有任何后门或绕过逻辑。
十六、数据收集与遥测:你的数据去了哪里
这是很多开发者关心的问题。源码揭示了完整的遥测架构。
16.1 三大数据出口
Claude Code CLI
│
├─ logEvent() → 事件队列
│ ├──→ Datadog (30+ 种事件)
│ │ POST https://http-intake.logs.us5.datadoghq.com/api/v2/logs
│ │ Token: pubbbf48e6d78dae54bceaa4acf463299bf
│ │
│ └──→ Anthropic 1P (完整事件+用户元数据)
│ POST https://api.anthropic.com/api/event_logging/batch
│ 失败时持久化到 ~/.claude/telemetry/1p_failed_events.*.json
│
└─ Metrics → BigQuery
POST https://api.anthropic.com/api/claude_code/metrics
16.2 发送到 Datadog 的事件(30+ 种)
API 相关: tengu_api_error, tengu_api_success
认证相关: tengu_oauth_success, tengu_oauth_error, token refresh 事件
工具使用: tengu_tool_use_success, tengu_tool_use_error
会话相关: tengu_session_file_read, tengu_exit, tengu_init
语音相关: tengu_voice_toggled, tengu_voice_recording_started
Chrome: chrome_bridge_connection_succeeded, chrome_bridge_tool_call_completed
团队同步: tengu_team_mem_sync_pull, tengu_team_mem_sync_push
配额变化: tengu_claudeai_limits_status_changed
16.3 收集的用户数据
每个事件附带的元数据:
// firstPartyEventLogger.ts — 1P 事件元数据
{
event_id: UUID,
event_name: '事件名',
client_timestamp: '时间戳',
device_id: '持久化设备ID', // getOrCreateUserID()
user_id: '同 device_id',
email: '用户邮箱', // OAuth 登录后可用
session_id: '会话ID',
core_metadata: {
service_name: 'claude-code',
version: '版本号',
platform: 'darwin/linux/win32',
os_info: 'OS版本'
},
user_metadata: {
organization_uuid: '组织ID',
account_uuid: '账户ID',
subscription_type: '订阅类型',
rate_limit_tier: 'API限制等级',
first_token_time: '首次使用时间'
},
auth: {
account_uuid: '账户UUID',
organization_uuid: '组织UUID'
}
}
16.4 GitHub CI 环境下的额外收集
当在 GitHub Actions 中运行时,额外收集:
// GrowthBook 用户属性
{
github: {
actor: 'GitHub 用户名',
actorId: 'GitHub 用户ID',
repository: '仓库名',
repositoryId: '仓库ID',
repositoryOwner: '仓库所有者',
repositoryOwnerId: '所有者ID'
}
}
16.5 代码内容保护
源码中有显式的代码泄漏防护:
// 元数据类型名本身就是防线——类型名强制开发者确认
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = string
// metadata.ts — 截断和清洗规则
- 字符串截断到 512 字符(只展示前 128 字符)
- JSON 输出上限 4KB
- 集合元素上限 20 个
- 嵌套深度上限 2 层
- 以 _ 开头的内部字段自动剥离
// 用户提示词默认 redacted
userPrompt = '<REDACTED>' // 除非显式开启 OTEL_LOG_USER_PROMPTS=1
// MCP 工具名默认匿名化
mcpToolName = 'mcp_tool' // 除非是官方 MCP 注册表中的工具
16.6 用户桶哈希
为了在 Datadog 中统计独立用户数同时保护隐私:
// datadog.ts — 用户 ID 哈希到 30 个桶
function getUserBucket(userId: string): number {
const hash = SHA256(userId)
return hash % 30 // 30 个桶,无法反推用户 ID
}
16.7 关闭遥测
Claude Code 提供了两个级别的关闭方式:
# 级别 1:关闭遥测(Datadog + 1P 事件 + 反馈调查)
export DISABLE_TELEMETRY=1
# 级别 2:关闭所有非必要网络请求(最彻底)
# 包括:遥测、自动更新、release notes、模型能力刷新
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
此外,组织管理员可以通过 API 在组织级别禁用指标收集:
GET /api/claude_code/organizations/metrics_enabled → false
16.8 事件采样
并非每个事件都会被发送——GrowthBook 配置了采样率:
// firstPartyEventLogger.ts
// tengu_event_sampling_config 配置每种事件的采样率
// 如果被采样掉,sample_rate 字段会被添加到元数据中
16.9 失败事件持久化
如果事件发送失败,会写入本地磁盘:
~/.claude/telemetry/1p_failed_events.<sessionId>.<uuid>.json
下次启动时重试发送(二次退避,最多 8 次尝试,最大间隔 30 秒)。
16.10 遥测小结

结论:Claude Code 收集的是使用行为数据,不是代码内容。有明确的类型系统防止开发者意外发送代码。可以通过环境变量完全关闭。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)