mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5718 字
16 分钟
Claude Code 深度解析
2026-06-22

在编程大模型的赛道上,Anthropic 的 Claude 系列几乎定义了行业标准。Cursor、GitHub Copilot 把它设为默认后端,国产模型发布时”对比 Claude”成了标配。它在 SWE-bench Verified 上拿下 95.00 登顶,几乎是”编程大模型”的天花板。

今年 3 月,安全研究员 Chaofan Shou 发现 Claude Code 的 npm 包里带着未剥离的 source map,完整源码由此暴露。分析仓库(liuup/claude-code-analysis)里 1,902 个源文件、513,237 行代码摆出来后,一个比例很扎眼:直接调用 LLM 的代码约占 5%,其余 95% 全部是围绕 LLM 的 harness 基础设施

这 95% 包含什么?权限、压缩、路由、协调、持久化、Hooks、安全防御、内存管理,每一项都是生产级 Agent 必须面对的工程问题。这篇文章从源码出发,一层层看这套基础设施的内部机制。

一、5% vs 95%:具体数字#

源码目录结构直接回答了”95% 是什么”:

目录文件数职责
src/tools/~30 目录所有工具实现(Bash、Read、Edit、Agent、Workflow……)
src/components/~800 文件React/Ink TUI 组件库
src/utils/~500 文件权限、CLAUDE.md、memory、hooks、effort、MCP、settings……
src/commands/~70 文件/ 命令实现
src/services/多目录compact、MCP、LSP、OAuth、plugins、rate limits……
src/skills/多文件Skills 系统加载与注册
src/coordinator/多文件多代理协调模式
src/memdir/多文件Memory 目录管理
src/hooks/多文件Hook 执行引擎与事件系统

真正与 LLM 对话的代码(API 客户端、提示词组装、流式处理、重试逻辑)散落在 src/services/api/src/assistant/ 中,占比极小。其余全部是”让 LLM 调用可控、可恢复、可审计”的工程基础设施。

下面一层层展开。

二、会话持久化:append-only JSONL#

Claude Code 的对话数据存储在 ~/.claude/projects/<path-hash>/<sessionId>.jsonl 里。这不是数据库,不是快照,而是一个 append-only 的事件日志。源码中 sessionStorage.tsProject 类管着整个写入生命周期。

写入:缓冲 + 批量刷盘#

写入不是逐条落盘。每条消息先进内存队列 writeQueues,定时器每 100ms(远程持久化时 10ms)触发一次 drainWriteQueue(),把队列里的所有条目序列化后拼成一个大字符串,通过一次 fsAppendFile 调用写入。单次写入上限 100MB(MAX_CHUNK_BYTES),超过就分片。

文件权限严格:所有文件 0o600(仅 owner 可读写),目录 0o700

会话文件也不是启动时就建的。sessionFile 初始为 null,消息先暂存在 pendingEntries 数组里。直到第一条真正的 user/assistant 消息出现,materializeSessionFile() 才创建文件并刷出暂存条目。这避免了只有 hook/attachment 消息时留下空会话文件。

消息类型与对话树#

JSONL 里每一行是一个 JSON 对象,type 字段标识消息类型。我在一个 1,364 行的真实会话文件里统计了各类型分布:

类型数量说明
assistant496AI 回复(含 tool_use 块)
user412用户输入或 tool_result
attachment73上下文注入(hook 输出、skill 列表、MCP 指令等)
ai-title70AI 生成的对话标题
last-prompt69最近一条 prompt 追踪
mode / permission-mode68 / 59模式变更记录
file-history-snapshot27文件修改快照
system/compact_boundary1压缩边界标记

消息之间通过 parentUuid/uuid 形成链表:每条消息的 parentUuid 指向前一条消息的 uuid,首条的 parentUuidnull。源码里 insertMessageChain() 逐条推进链游标:

// 简化逻辑
let parentUuid = startingParentUuid ?? null
for (const message of messages) {
const transcriptMessage = {
parentUuid: isCompactBoundary ? null : effectiveParentUuid,
logicalParentUuid: isCompactBoundary ? parentUuid : undefined,
...message,
}
await this.appendEntry(transcriptMessage)
if (isChainParticipant(message)) {
parentUuid = message.uuid // 推进链游标
}
}

这条链有两个设计细节值得单独讲,都跟”什么能进链、什么不能进”有关。

第一个是 Progress 消息不参与链。源码注释说得很直白:“Progress messages are NOT transcript messages. They are ephemeral UI state and must not be persisted to the JSONL or participate in the parentUuid chain. Including them caused chain forks that orphaned real conversation messages on resume.” 也就是说,Progress 是给 UI 看的临时状态,一旦写进 JSONL 还接上链,恢复会话时就会把真正的对话消息挤成孤儿。

第二个是压缩边界会截断链。compact_boundary 消息的 parentUuid 被设为 null,这样 --continue 不会走进压缩前的历史。但 logicalParentUuid 保留了真实父节点,用于逻辑追踪。这是一个很有意思的取舍:物理链断开是为了恢复时轻装,逻辑链保留是为了需要时还能回溯。

元数据尾部重追加#

--resume 的快速会话列表只需要读元数据(标题、标签、模式),不需要解析整个 JSONL。源码用 readHeadAndTail() 只读文件头尾各 64KB(LITE_READ_BUF_SIZE = 65536),通过原始字符串搜索提取元数据字段,不做完整 JSON 解析。

问题来了:如果用户在对话中途用 /rename 改了标题,后续追加的消息可能把 custom-title 条目推出 64KB 尾部窗口,导致 --resume 看不到新标题。

解决办法是 reAppendSessionMetadata():在压缩时和会话退出时,把所有当前元数据(last-prompt、custom-title、tag、agent-name、mode 等)重新追加到文件末尾。这样尾部窗口内始终有最新的元数据。

AI 生成的标题(ai-title)从不重追加。源码注释解释:“reAppendSessionMetadata never re-appends AI titles (they’re ephemeral/regeneratable; re-appending would clobber user renames on resume).” 这又是一个显式优于隐式的选择:用户改过的名字不能被 AI 重新生成的标题盖掉。

Sidechain 分离#

子代理的对话写独立的 JSONL 文件,路径为 <session-dir>/subagents/agent-<agentId>.jsonl。所有子代理消息带 isSidechain: trueagentId 字段。

源码里一个关键设计:sidechain 的本地写入跳过 UUID 去重。因为 fork 继承的父消息与主会话共享 UUID,如果对主会话的 UUID 集做去重,会丢弃 sidechain 中从主会话继承的消息,导致 sidechain 日志不完整。

文件历史版本化#

~/.claude/file-history/<sessionId>/ 下存的是文件修改前的完整内容快照(不是 diff)。备份文件命名为 <sha256(path)>.slice(0,16)@v<N>,N 是版本号。

fileHistoryTrackEdit() 在文件被修改前调用,创建 v1 备份。每轮对话结束后 fileHistoryMakeSnapshot() 检查所有被追踪文件是否变化(stat 比较 + 内容比较兜底),变化的文件递增版本号。/rewind 通过恢复对应版本的完整内容来回退文件修改。

到这里可以看出,会话、元数据、子代理、文件历史全挂在文件系统上。Claude Code 之所以能”有项目上下文、有历史经验、有行为约束”,靠的不是数据库,而是这一堆约定好结构的文件。后面 CLAUDE.md 和 memory 也是同一条路子。

三、CLAUDE.md 与 memory:内部加载机制#

CLI 和命令那篇文章介绍了 CLAUDE.md 和 memory 的用途和文件格式。源码揭示了它们内部的加载逻辑和信任模型。

四级信任模型#

claudemd.ts 文件顶部的注释记了四级加载顺序:

  1. Managed/etc/claude-code/CLAUDE.md(Linux)、/Library/Application Support/ClaudeCode(macOS),系统管理员级,始终加载,不可被排除
  2. User~/.claude/CLAUDE.md,用户全局级,可引用外部文件
  3. Project:项目根目录的 CLAUDE.md.claude/CLAUDE.md.claude/rules/*.md,项目级,引用外部文件需显式批准
  4. Local:项目根目录的 CLAUDE.local.md,本地级,不进 git

另外两个特殊层级:

  • AutoMem~/.claude/projects/<slug>/memory/MEMORY.md,自动记忆入口
  • TeamMem<autoMemPath>/team/MEMORY.md,团队共享记忆(需 GrowthBook 特性开关 tengu_herring_clock

加载顺序从高信任到低信任,但优先级反序:后加载的文件覆盖先加载的。Project 和 Local 文件按目录从根到 CWD 遍历,越靠近 CWD 的文件加载越晚、优先级越高。这种”信任高先加载、优先级低先被覆盖”的安排,让项目级规则能压过用户全局规则,本地级又能压过项目级,符合”离代码越近说话越算数”的直觉。

@include 指令#

CLAUDE.md 支持 @./relative/path 语法引用外部文件。源码里 extractIncludePathsFromTokens() 用 marked Lexer(gfm: false 避免 ~/path 被误解析为删除线)解析 @path 引用,递归处理。

关键限制:

  • 深度上限MAX_INCLUDE_DEPTH = 5,防循环引用和 DoS
  • 文件扩展名白名单:约 80 种文本文件扩展名(.md.ts.py.go 等),二进制不加载
  • 循环引用防护processedPaths Set 追踪已处理路径
  • 不存在的文件:静默跳过

条件规则#

.claude/rules/*.md 里的规则文件可以在 frontmatter 声明 paths: 字段,使其成为条件规则:

---
paths:
- "src/**/*.ts"
- "tests/**/*.ts"
---

只有当 Claude Code 操作匹配路径模式的文件时,该规则才会被加载。源码用 ignore npm 包做 glob 匹配。这把”规则总是全量加载”改成了”按需触发”,规则文件可以写得很细而不拖慢每次启动。

Memory 的相关性选择#

findRelevantMemories() 的实现揭示了一个精巧设计:它不是简单加载所有 memory 文件,而是用一个轻量级 Sonnet 查询来挑哪些 memory 与当前任务相关。

流程:

  1. scanMemoryFiles() 扫描 memory 目录,读每个 .md 文件的前 30 行(frontmatter),最多 200 个文件
  2. selectRelevantMemories() 构造一个查询发给 Sonnet(不是主模型,为了省钱),输入包括当前任务描述、所有 memory 文件的 manifest(文件名、类型、时间戳、描述),要求返回最多 5 个最相关的文件名
  3. 返回的文件名与实际文件集做校验,防幻觉

几个设计决策值得注意:

  • getDefaultSonnetModel() 而非主模型,max_tokens: 256,极小的输出预算
  • recentTools 参数:当 Claude 正在使用某个工具时,跳过该工具的使用参考类 memory(但保留关于该工具的陷阱/警告类 memory)
  • alreadySurfaced 参数:防止重复选择之前轮次已经加载的 memory

Session memory 的沙箱提取#

Session memory 的提取由一个沙箱子代理完成。源码里 sessionMemory.ts 注册为 postSamplingHook,在每次模型回复后检查是否需要更新 session memory。

提取时通过 runForkedAgent() 创建隔离上下文,只允许 FileEditTool 操作精确的 memory 文件路径,其他所有工具都被拒绝。这防止了提取子代理越权修改项目文件。

自动记忆提取(extractMemories.ts)也用 forked agent,但权限稍宽:允许 Read、Grep、Glob(无限制)、只读 Bash、Edit/Write 限定在 auto-memory 目录内。硬性限制:最多 5 轮(maxTurns: 5),且如果主代理已经写了 memory 文件,forked agent 跳过(互斥)。这个互斥很关键:两个代理同时写同一个 memory 文件只会打架,让主代理说了算、forked 只在主代理没动时补刀,避免了写冲突。

四、Hooks 系统:生命周期拦截#

Hooks 是 Claude Code 里最强大也最欠文档化的自动化层。源码揭示了 27 个 hook 事件类型和 4 种执行模式。

Hook 事件类型#

源码 coreTypes.ts 里定义了完整的 HOOK_EVENTS 列表:

事件触发时机匹配键
PreToolUse工具调用前tool_name
PostToolUse工具调用成功后tool_name
PostToolUseFailure工具调用失败后tool_name
PermissionRequest权限请求时tool_name
PermissionDenied权限被拒绝时tool_name
PreCompact上下文压缩前trigger
PostCompact上下文压缩后trigger
SessionStart会话启动时source
SessionEnd会话结束时reason
Stop查询循环停止时
StopFailure查询循环异常停止时error
SubagentStart子代理启动时agent_type
SubagentStop子代理停止时agent_type
UserPromptSubmit用户提交 prompt 时
Notification通知事件notification_type
Setup初始化时trigger
TeammateIdle队友空闲时
TaskCreated任务创建时
TaskCompleted任务完成时
ElicitationMCP 弹出交互时mcp_server_name
ElicitationResultMCP 交互结果mcp_server_name
ConfigChange配置变更时source
WorktreeCreateWorktree 创建时
WorktreeRemoveWorktree 移除时
InstructionsLoadedCLAUDE.md 加载时load_reason
CwdChanged工作目录变更时
FileChanged文件变更时basename(file_path)

每个事件可以通过匹配键过滤。比如 PreToolUse 的 hook 可以只匹配 tool_name: "Bash",不碰其他工具。这张表覆盖了从会话生命周期到工具调用到文件变更的全部关键节点,相当于把 Claude Code 的运行过程切成了一串可挂载的切面。

四种执行模式#

类型type 字段说明默认超时
Shell 命令command启动子进程,hook 输入 JSON 通过 stdin 传入10 分钟
LLM 查询prompt用轻量模型做单轮判断,返回 {"ok": true/false}30 秒
Agent 验证agent用完整 query() 做多轮验证(最多 50 轮)60 秒
HTTP 回调httpPOST hook 输入 JSON 到指定 URL10 分钟

Shell 命令的退出码语义:0 = 成功,2 = 阻塞(把 stderr 展示给模型,阻止工具调用),其他 = 非阻塞错误(只展示给用户)。

LLM 查询模式禁用 thinking(thinkingConfig: { type: 'disabled' }),确保快速响应。Agent 模式获得所有工具(减去 ALL_AGENT_DISALLOWED_TOOLS)加上 SyntheticOutputTool,在 dontAsk 权限模式下运行。

SSRF 防护#

HTTP hooks 有严格的 SSRF 防护。ssrfGuard.ts 实现了自定义 dns.lookup 函数,确保 DNS 验证和实际连接使用同一个 IP,不存在重绑定窗口。

被阻止的 IP 范围包括所有私有地址:10.0.0.0/8172.16.0.0/12192.168.0.0/16169.254.0.0/16(云元数据)、100.64.0.0/10(CGNAT,覆盖阿里云元数据 100.100.100.200)等。IPv6 同样阻止 fc00::/7fe80::/10 等本地范围。127.0.0.0/8::1(loopback)显式放行,支持本地开发。

两个绕过条件:sandbox proxy 活跃时(proxy 自身有域名白名单),或 HTTP_PROXY/HTTPS_PROXY 设置时(proxy 负责 DNS 解析)。

信任要求#

所有 hooks 在交互模式下要求工作区信任。源码注释记录了历史漏洞:SessionEnd hooks 曾在用户拒绝信任对话框时仍然执行,SubagentStop hooks 曾在信任确认前执行。现在这些都改成集中式安全检查。这又是一个 fail-closed 的实例:信任没确认前,hook 一个都不跑。

五、工具执行管线#

前面 hooks 是用户可挂载的切面。工具调用本身还有一条内部管线,在源码 toolExecution.tscheckPermissionsAndCallTool() 里。这是全文信息密度最高的一段,决定了每一次工具调用从输入到结果要走完哪些步骤。

八步管线#

  1. Schema 验证tool.inputSchema.safeParse(input),Zod schema 校验。如果失败,检查是否因为 schema 被延迟加载(deferred tool 场景),给出提示先调用 ToolSearch
  2. 自定义验证tool.validateInput?.(parsedInput, toolUseContext),工具可以定义额外的语义校验
  3. Backfilltool.backfillObservableInput() 填充派生字段(如 SendMessageTool 补充字段,文件工具展开路径)。原始模型输入单独保留,因为修改它会影响序列化 transcript 和 VCR fixture 哈希
  4. PreToolUse hooksrunPreToolUseHooks() 生成器产出结果,包括权限决策、输入修改、附加上下文、阻塞信号等
  5. 权限决策resolveHookPermissionDecision() 将 hook 的权限结果与 settings.json 规则合并。这里有一条硬约束:hook 的 ‘allow’ 不能绕过 settings.json 的 deny/ask 规则
  6. tool.call():执行工具本体
  7. PostToolUse hooks:工具调用成功后触发。MCP 工具的输出可以被 hook 修改
  8. PostToolUseFailure hooks:工具调用失败时触发,提供 tool_nametool_inputerrorerror_type

前半段是输入校验和权限管控,中间是实际执行,后半段是结果处理的扩展点。下面这张图把八步串起来,重点看权限决策那一步如何与 settings.json 合并。

flowchart TD input([工具调用输入]) --> schema[Schema 验证<br>Zod safeParse] schema -->|失败| deferred{延迟加载?} deferred -->|是| hint[提示调用 ToolSearch] deferred -->|否| fail1([返回错误]) schema -->|通过| validate[自定义验证<br>validateInput] validate -->|失败| fail2([返回错误]) validate -->|通过| backfill[Backfill<br>填充派生字段] backfill --> pre_hook[PreToolUse hooks<br>权限决策/输入修改/阻塞] pre_hook --> perm[权限决策<br>hook 结果与 settings.json 合并] perm -->|允许| call[tool.call 执行] perm -->|拒绝| denied([权限拒绝]) call -->|成功| post_hook[PostToolUse hooks] call -->|失败| post_fail[PostToolUseFailure hooks] post_hook --> result([返回结果]) post_fail --> error_result([返回错误]) style denied fill:#ffebee,stroke:#f44336 style call fill:#e8f5e9,stroke:#4caf50

第 5 步的权限决策是最值得展开的,因为它把两套独立的权限来源(hook 和 settings.json)合并成最终决议,合并规则直接决定了安全边界。

flowchart TD start([工具调用请求]) --> hook_check{PreToolUse hook<br>返回权限决策?} hook_check -->|hook 返回 deny| hook_deny[hook deny] hook_check -->|hook 返回 allow| hook_allow[hook allow] hook_check -->|hook 无决策| settings[检查 settings.json 规则] settings --> rule_deny{规则 deny?} rule_deny -->|是| final_deny[最终: 拒绝] rule_deny -->|否| rule_ask{规则 ask?} rule_ask -->|是| prompt_user[弹出权限确认] rule_ask -->|否| rule_allow[规则 allow 或无匹配] hook_allow --> override_check{settings.json<br>有 deny/ask 规则?} override_check -->|有 deny| final_deny override_check -->|有 ask| prompt_user override_check -->|无| final_allow[最终: 允许] hook_deny --> final_deny prompt_user -->|用户允许| final_allow prompt_user -->|用户拒绝| final_deny rule_allow --> final_allow style final_deny fill:#ffebee,stroke:#f44336 style final_allow fill:#e8f5e9,stroke:#4caf50 style prompt_user fill:#fff3e0,stroke:#ff9800

图里右侧那条 “hook allow → 检查 deny/ask” 的路径,就是下面这条约束的具象化。

Warning

核心约束:hook 的 allow 不能绕过 settings.json 的 deny/ask 规则。即使 hook 返回 allow,只要 settings.json 里存在对应的 deny 规则,调用仍然会被拒绝。这是 fail-closed 在权限系统里的直接体现。

并发分批#

partitionToolCalls() 把同一轮的多个工具调用按并发安全性分批:

  • 每个工具通过 tool.isConcurrencySafe(parsedInput) 声明是否可并发
  • 连续的可并发工具合并为一个批次,runToolsConcurrently()all() 并行执行,上限 10(CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY
  • 不可并发工具各自独占一个批次,runToolsSerially() 串行执行

流式执行器#

StreamingToolExecutor 是工具在流式响应中被处理的状态机(不等完整响应返回就开始执行工具)。状态流转:queued -> executing -> completed -> yielded

并发控制:新工具可以开始执行,当且仅当没有工具在运行,或者它自身和所有正在运行的工具都是并发安全的。

兄弟错误级联:只有 Bash 错误会取消兄弟工具。源码注释解释:“Bash commands often have implicit dependency chains (e.g. mkdir fails -> subsequent commands pointless). Read/WebFetch/etc are independent — one failure shouldn’t nuke the rest.” 这是个很贴心的判断:Bash 命令之间常有隐式依赖(mkdir 失败后续命令就没意义了),而 Read、WebFetch 互相独立,一个失败不该连累其他。

六、effort 与 thinking 系统#

Effort 解析链#

源码 effort.tsresolveAppliedEffort() 按优先级依次回退:先看 CLAUDE_CODE_EFFORT_LEVEL 环境变量,没有就取 appState.effortValue,再没有就用模型默认值。

4 个级别:lowmediumhighmax。如果环境变量为 'unset''auto',返回 undefined(不发 effort 参数给 API,由 API 决定默认值)。

模型支持检查:

  • modelSupportsEffort():目前 Opus 4.6 和 Sonnet 4.6 支持。1P provider 默认 true,3P 默认 false
  • modelSupportsMaxEffort():公开模型中仅 Opus 4.6 支持 max。内部(ant)模型可以覆盖
  • getDefaultEffortForModel():Opus 4.6 对 Pro 订阅默认 medium(GrowthBook 开关 tengu_grey_step2 控制是否扩展到 Max/Team)

如果解析结果为 max 但模型不支持,静默降级到 highCLAUDE_EFFORT 环境变量(暴露给 hooks 和 Bash)反映的是降级后的值。这个”对外暴露降级后的值”很重要:hook 拿到的是实际生效的 effort,不是用户原本想要的,hook 据此做判断才不会落空。

持久化规则:lowmediumhigh 可持久化。max 对外部用户仅会话级,不可持久化。内部用户可用数字值(1-100,映射:<=50=low, <=85=medium, <=100=high, >100=max),数字值从不持久化。

Ultrathink#

thinking.ts 里 ultrathink 的门控:

export function isUltrathinkEnabled(): boolean {
if (!feature('ULTRATHINK')) return false // 编译时开关
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_turtle_carbon', true) // GrowthBook 运行时开关
}

关键词检测:/\bultrathink\b/i。当用户输入包含 “ultrathink” 时,effort 提升到 high。

Thinking 配置发给 API 的格式:

{
"thinking": {
"type": "adaptive",
"budget_tokens": "<按 effort 级别动态计算>"
}
}

adaptive 是当前推荐类型(enabled 已废弃)。CLAUDE_CODE_DISABLE_THINKINGCLAUDE_CODE_DISABLE_ADAPTIVE_THINKING 可以关闭 thinking。

源码注释里有一条重要警告:“IMPORTANT: Do not change default thinking enabled value without notifying the model launch DRI and research. This can greatly affect model quality and bashing.” 以及 “Newer models (4.6+) are all trained on adaptive thinking and MUST have it enabled for model testing.” 也就是说,adaptive thinking 不是可选项而是 4.6+ 模型的训练前提,关掉它等于让模型在没训练过的模式下跑,质量会掉。

七、安全架构#

前面几节已经多次出现 fail-closed 的默认值。源码里还散布着几层从未公开文档化的安全防御。

Unicode 隐写防御#

sanitization.ts 实现了对 Unicode 隐写攻击的防护,直接源于 HackerOne 报告 #3086545。该漏洞里,攻击者用 Unicode Tag 字符在 MCP 工具输入中注入对模型可见但对用户不可见的指令。

partiallySanitizeUnicode() 的处理流程:

  1. 迭代式 NFKC Unicode 归一化(处理组合字符序列)
  2. 移除危险 Unicode 属性类:\p{Cf}(格式控制符)、\p{Co}(私用区)、\p{Cn}(未分配码位)
  3. 显式字符范围兜底(某些环境不支持 Unicode 属性类正则):零宽空格、方向控制符、方向隔离符、BOM、BMP 私用区
  4. 最多迭代 10 次(MAX_ITERATIONS = 10),达到上限则抛出错误

recursivelySanitizeUnicode() 递归处理对象和数组中的所有字符串字段。此防护始终启用,应用于所有 MCP 工具输入。

PII 类型屏障#

src/services/analytics/index.ts 里有一个类型别名:

type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = ...

这个夸张的命名迫使开发者通过 as 断言显式确认数据对遥测安全。它不是技术限制,而是一个人为的流程屏障:你要发遥测数据,必须先声明”我确认这不是代码或文件路径”。

信任边界时序安全#

遥测和完整环境变量只在工作区信任确认后才激活。这防止了恶意 CLAUDE.md 通过环境变量注入在信任确认前就触发遥测上报。

Fail-closed 默认值#

整个系统遵循 fail-closed:分类器无法解析 = 阻止,API 错误 = 阻止,权限规则冲突 = 拒绝。buildTool() 工厂函数默认所有工具 isConcurrencySafe: falseisReadOnly: false,工具必须显式声明自己的安全属性。换句话说,工具默认是”不安全”的,要获得并发或只读的待遇,得自己举证,不是默认享受。这套默认值加上前面 hook 信任、权限合并的约束,构成了 Claude Code 在异常情况下倾向拒绝而非放行的完整链条。

八、5% vs 95% 回看#

回到开头那个比例。5% 的代码只负责”与模型对话”,95% 的代码负责”让对话可控”。API 客户端、提示词组装、流式处理是薄壳,权限、压缩、持久化、Hooks、安全防御是厚壁。LLM 本身不可靠(幻觉、注入、越权),harness 的职责就是在不可靠的核心外围把可靠性建起来。

这套思路对自建 Agent 团队有直接含义:不要把精力花在 5% 的 LLM 调用层上,那部分 Anthropic/OpenAI 的 SDK 已经做好了。工程重心应该放在 95% 的 harness 上,权限系统、上下文管理、持久化与恢复、工具执行管线、安全防御。这些才是 Agent 产品差异化的战场。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Claude Code 深度解析
https://blog.souloss.cn/posts/ai/agents-decoded/claude-code/claude-code-decoded/
作者
Souloss
发布于
2026-06-22
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时