5.0 章节导言
想象你走进一家餐厅,服务员热情地迎上来,但你每点一道菜,他都会问:"请问您叫什么名字?您坐在哪一桌?您之前点过什么菜?"——你会觉得这家餐厅疯了。
一个优秀的服务员应该记住你的桌号、你已点的菜品、你的特殊偏好。这种"记忆"不是智力问题,而是状态管理问题。
Claude Code 同样需要"记住"很多事情:你是谁、你之前让我做了什么、我们花了多少钱、当前在哪个目录工作、使用了什么模型。这些信息构成了一个复杂的会话状态,它需要在整个对话过程中被创建、维护、查询和更新。
但与餐厅服务员不同,Claude Code 的状态管理面临着独特的挑战:
- 状态维度极多(100+ 个字段)
- 状态的生命周期可能跨多个会话
- 状态需要在成本追踪、性能优化和功能实现之间取得平衡
- 状态必须支持并发访问(多个工具可能同时读写状态)
这一章,我们将深入 Claude Code 的状态管理系统,看看它是如何优雅地应对这些挑战的。
5.1 全局状态的架构设计
5.1.1 Vj7 工厂函数:状态对象的诞生
Claude Code 的全局状态不是通过简单的对象字面量创建的,而是通过一个工厂函数 Vj7() 生成的。这个函数返回一个状态对象 G8,它是整个应用的单一状态源(Single Source of Truth)。
// 伪代码示意
const G8 = Vj7();
// G8 现在包含了整个应用的所有状态为什么要用工厂函数而不是直接定义一个对象?这是一个经典的软件工程决策,它带来几个关键优势:
延迟初始化:工厂函数可以根据实际需要来初始化状态,而不是在应用启动时就创建所有状态。有些状态字段可能只有在特定功能被触发时才需要初始化。
默认值计算:某些状态字段的默认值不是静态的,而是需要根据环境计算。比如
sessionId需要生成 UUID,modelUsage需要根据历史数据初始化。不变量保证:工厂函数可以在创建状态时验证不变量(invariants),确保状态的初始值是合法的。
封装创建逻辑:将状态的创建逻辑集中在一个地方,而不是散布在整个代码库中。这符合"信息隐藏"的原则。
5.1.2 100+ 字段的巨型状态对象
G8 全局状态对象包含了超过 100 个字段。在一个对象中管理这么多字段,听起来像是维护噩梦,但实际上 Claude Code 通过逻辑分组和职责分离让这个巨型对象变得可管理。
我们可以将这些字段分为几个逻辑域:
身份域(Identity Domain)
{
sessionId: string, // 当前会话唯一标识(UUID)
parentSessionId: string, // 父会话标识(会话链)
agent: AgentDefinition, // 当前 Agent 定义
agentDefinitions: Map, // 所有已注册的 Agent 定义
}这些字段定义了"我是谁"——当前会话的身份、它从哪里来(parentSessionId 暗示了会话的继承关系)、它扮演什么角色(agent)。
成本域(Cost Domain)
{
totalCostUSD: number, // 总成本(美元)
modelUsage: ModelUsageMap, // 各模型的使用统计
tokenCounter: TokenCounter, // Token 计数器
maxBudgetUsd: number, // 最大预算
fastMode: boolean, // 快速模式(降低成本)
}成本域是状态管理中最精细的部分之一——它追踪每一分钱的去向,确保 AI 的使用不会超出预算。
交互域(Interaction Domain)
{
isInteractive: boolean, // 是否为交互模式
isRemoteMode: boolean, // 是否为远程模式
kairosActive: boolean, // Kairos 功能是否激活
sdkBetas: string[], // SDK Beta 功能列表
}交互域控制 Claude Code 与用户的交互方式——是交互式还是批处理,是本地还是远程。
缓存域(Cache Domain)
{
promptCache1hAllowlist: Set, // 1小时缓存白名单
promptCache1hEligible: Set, // 1小时缓存候选集
}缓存域管理 prompt 缓存策略——哪些 prompt 片段可以被缓存以节省成本。
权限域(Permission Domain)
{
toolPermissionContext: PermissionContext, // 工具权限上下文
}权限域管理每个工具的访问控制状态——哪些工具需要用户确认、哪些可以自动执行。
配置域(Configuration Domain)
{
// 各种配置项...
}配置域包含了所有的用户偏好、项目配置、系统设置等。
5.1.3 不可变与可变的分离
在 100+ 个字段中,有些在会话创建后就不再改变(比如 sessionId),有些则在每次交互中都会更新(比如 totalCostUSD、tokenCounter)。Claude Code 对这两类状态采用了不同的管理策略。
**不可变状态(Immutable State)**包括:
sessionId:一旦生成就不会改变parentSessionId:会话的血缘关系不会改变- 初始配置项
这些状态在工厂函数中一次性初始化,之后被视为只读。任何试图修改这些字段的代码都应该被视为 bug。
**可变状态(Mutable State)**包括:
totalCostUSD:每次 API 调用后更新modelUsage:每次 API 调用后更新tokenCounter:每次 API 调用后更新toolPermissionContext:每次工具调用后可能更新
这些状态通过特定的方法来修改,而不是直接赋值。这种受控的可变性确保了状态变更的可追踪性和一致性。
5.1.4 单一状态源 vs. 派生状态
G8 对象并非存储了所有可能需要的信息。很多信息是派生的(derived)——它们可以从基础状态计算得出,但不需要单独存储。
例如:
- "当前会话是否超预算"可以从
totalCostUSD和maxBudgetUsd计算得出 - "缓存命中率"可以从
modelUsage中的缓存 token 数和总 token 数计算得出
这种区分非常重要:
- 基础状态存储在
G8中,是数据的"真相" - 派生状态通过函数或 getter 计算,确保与基础状态保持同步
如果缓存命中率也被单独存储,就可能出现基础状态更新后派生状态未更新的不一致情况。让派生状态始终从基础状态实时计算,消除了这类 bug 的可能性。
5.2 会话生命周期
5.2.1 Session:UUID 背后的身份故事
每个 Claude Code 会话都有一个唯一的 sessionId,它是一个 UUID(Universally Unique Identifier)。UUID 的使用确保了:
- 全局唯一性:即使在分布式环境中,两个会话的 sessionId 也不会重复。
- 无序性:UUID 不包含时间信息,无法从 sessionId 推断会话的创建时间。
- 不可猜测性:UUID v4 是随机生成的,无法预测下一个 sessionId。
sessionId: crypto.randomUUID() // 类似这样的生成方式但 sessionId 不仅仅是一个标识符——它是会话状态的生命线。所有的日志、审计记录、缓存条目都以 sessionId 为索引。如果 sessionId 丢失或错误,整个会话的状态就无法正确恢复。
5.2.2 ParentSessionId:会话的血缘关系
parentSessionId 是一个容易被忽视但极其重要的字段。它建立了一个会话链(session chain):
Session A (parentSessionId: null)
└── Session B (parentSessionId: A)
└── Session C (parentSessionId: B)这种血缘关系有什么用?
- 上下文继承:子会话可以继承父会话的某些状态(比如工作目录、环境变量、用户偏好)。
- 审计追踪:通过追踪会话链,可以重建完整的操作历史——"这个文件修改是在哪个会话链中发起的?"
- 成本归因:如果一个子 Agent(通过 Agent 工具创建的子会话)消耗了大量 token,通过 parentSessionId 可以追溯到发起这个 Agent 的原始会话。
- 会话恢复:如果主会话意外中断,通过 parentSessionId 可以找到最近的子会话状态,加速恢复过程。
这种"树状会话结构"在 Claude Code 的 Agent 系统中尤为重要。当你让 AI 使用 Agent 工具创建一个子 Agent 时,子 Agent 实际上就是一个新的会话,它的 parentSessionId 指向创建它的父会话。
5.2.3 switchSession:会话的迁移与持久化
Claude Code 支持在多个会话之间切换。switchSession 操作涉及以下几个步骤:
- 保存当前状态:将当前会话的状态序列化并持久化(通常是写入磁盘)
- 恢复目标状态:从持久化存储中加载目标会话的状态
- 更新全局引用:将
G8对象更新为目标会话的状态 - 触发事件:通知系统中的其他组件会话已切换
这个过程的关键挑战是一致性——在切换过程中,必须确保不会有部分组件使用旧状态、部分组件使用新状态的情况。
会话的持久化格式通常是一个 JSON 文件,存储在 Claude Code 的数据目录中。文件名可能基于 sessionId,例如 session-{uuid}.json。持久化内容包括:
- 所有可变状态的当前值
- 消息历史(对话记录)
- 工具调用记录
- 成本统计快照
5.2.4 会话的终止
会话的终止不是简单的"丢弃状态"。Claude Code 在会话终止时可能会执行以下操作:
- 写入审计日志:记录本次会话的所有关键操作
- 更新统计信息:将本次会话的使用统计汇总到全局统计中
- 清理临时资源:删除会话期间创建的临时文件
- 释放缓存:将会话特定的缓存条目标记为可回收
- 保存会话快照:为后续可能的恢复保存状态快照
5.3 成本追踪系统
5.3.1 totalCostUSD:每一分钱的去向
totalCostUSD 是一个递增的计数器,记录当前会话消耗的总成本(美元)。每次 API 调用后,系统会根据返回的 usage 信息更新这个值。
// 伪代码
afterApiCall(response) {
const inputCost = response.usage.input_tokens * INPUT_PRICE_PER_TOKEN;
const outputCost = response.usage.output_tokens * OUTPUT_PRICE_PER_TOKEN;
G8.totalCostUSD += inputCost + outputCost;
}totalCostUSD 的精确性至关重要——它是用户信任 Claude Code 的基础。如果成本计算不准确,用户可能会在不知情的情况下超出预算。
5.3.2 modelUsage:多维度的使用画像
modelUsage 不仅仅是一个简单的数字——它是一个结构化的统计数据,记录了每个模型的详细使用情况:
modelUsage: {
"claude-sonnet-4-20250514": {
inputTokens: 150000,
outputTokens: 45000,
cacheCreationInputTokens: 50000,
cacheReadInputTokens: 80000,
totalCalls: 120,
totalCostUsd: 2.35
},
"claude-haiku-3-20250307": {
inputTokens: 30000,
outputTokens: 10000,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 20000,
totalCalls: 15,
totalCostUsd: 0.15
}
}这种多维度的追踪有几个目的:
- 成本优化建议:如果发现某个模型的使用比例不合理(比如用 Opus 做简单的文件读取),可以建议用户切换到更便宜的模型。
- 缓存效率分析:通过比较
cacheCreationInputTokens和cacheReadInputTokens,可以评估缓存策略的效果。 - 预算规划:了解每个模型的成本占比,帮助用户合理分配预算。
注意 cacheCreationInputTokens 和 cacheReadInputTokens 的区分:
- 创建(creation):首次发送的 prompt token,需要付费写入缓存
- 读取(read):后续请求中从缓存读取的 token,价格更低
这个区分对成本优化非常重要。Claude Code 的 promptCache1hAllowlist 和 promptCache1hEligible 字段就是用来管理缓存策略的——哪些 prompt 片段值得缓存(因为它们会被频繁重用),哪些不值得。
5.3.3 缓存策略:promptCache1h 的奥秘
Claude API 支持两种 prompt 缓存:
- 5分钟缓存(ephemeral_5m):短期缓存,适用于会话内连续的对话轮次
- 1小时缓存(ephemeral_1h):长期缓存,适用于跨会话的 prompt 复用
promptCache1hAllowlist 和 promptCache1hEligible 管理的是 1 小时缓存:
- Allowlist(白名单):明确允许被缓存的 prompt 片段。这些通常是系统提示词(system prompt)和工具定义——它们在会话之间几乎不变。
- Eligible(候选集):可能适合缓存的 prompt 片段。系统会根据实际使用频率来决定是否将它们提升到白名单。
这种两级策略是一种渐进式优化——不是一开始就把所有东西都缓存,而是根据实际使用情况动态调整缓存策略。
从 Agent 工具的输出类型中,我们可以看到缓存成本的详细追踪:
usage: {
cache_creation: {
ephemeral_1h_input_tokens: number;
ephemeral_5m_input_tokens: number;
} | null;
}这表明 Claude Code 不仅仅使用缓存,还在精细追踪缓存的成本和效果——每一级缓存消耗了多少 token,为成本优化决策提供数据支持。
5.3.4 fastMode:成本与质量的权衡开关
fastMode: booleanfastMode 是一个全局开关,当启用时,Claude Code 会倾向于使用更快但可能质量稍低的操作方式。这可能包括:
- 使用 Haiku 模型处理简单的工具调用结果解析
- 减少上下文窗口中的历史消息数量
- 跳过某些非关键的分析步骤
- 使用更少的 token 来描述工具执行结果
fastMode 本质上是一个成本-质量帕累托最优的预设——它不是简单地"降低质量",而是在保持可接受质量的前提下最大化速度和成本效率。
5.4 Token 计数与预算控制
5.4.1 tokenCounter:不只是数数字
tokenCounter: {
input: number,
output: number,
cacheRead: number,
cacheWrite: number,
// 可能还有更多维度
}tokenCounter 追踪的不是简单的"用了多少 token",而是多维度的 token 使用情况。这种区分很重要,因为不同类型的 token 有不同的成本和含义:
- Input tokens:发送给模型的 token,成本较高
- Output tokens:模型生成的 token,成本最高
- Cache read tokens:从缓存读取的 token,成本最低
- Cache write tokens:写入缓存的 token,有一次性成本
通过追踪这些不同的维度,Claude Code 可以:
- 准确计算每次 API 调用的成本
- 评估缓存策略的效果(cacheRead 的比例越高越好)
- 预测下一次 API 调用的成本(基于历史模式)
- 在接近预算上限时自动调整行为(比如切换到更便宜的模型)
5.4.2 maxBudgetUsd:预算的硬边界
maxBudgetUsd: number // 例如 5.00 表示5美元上限当 totalCostUSD 接近 maxBudgetUsd 时,Claude Code 需要做出决策。这不是一个简单的"到了就停"的逻辑,而是一个渐进式的响应策略:
- 80% 预算:开始警告 AI 注意成本,鼓励更高效的工具使用
- 90% 预算:自动启用 fastMode,减少非必要的操作
- 95% 预算:限制使用昂贵的模型(Opus)
- 100% 预算:硬性停止——拒绝新的 API 调用
这种渐进式的策略比简单的"用完就停"更加人性化。它给用户和 AI 留出了调整的空间,而不是在一个关键操作进行到一半时突然中断。
5.4.3 Token 预算与工具使用的交互
工具的使用直接影响 token 消耗。每次工具调用,其结果都会被包含在下一轮 API 请求的上下文中。这意味着:
- 读取一个大文件(比如 10000 行的日志)→ 消耗大量 input token
- 执行一个返回大量输出的命令 → 消耗大量 input token
- 使用 head_limit 限制搜索结果 → 节省 input token
Claude Code 的工具设计中的很多"限额"机制(Grep 的 head_limit、Glob 的 truncated),从根本上说是token 预算管理的一部分。限制工具输出的不仅仅是性能考虑,更是成本控制。
这种"工具层面嵌入成本意识"的设计,是 Claude Code 与简单的 LLM 封装器之间的关键区别。
5.5 事件系统
5.5.1 Dz() 发布订阅:状态的神经系统
如果说 G8 是 Claude Code 的"大脑",那么事件系统 Dz() 就是它的"神经系统"——它负责在系统的各个组件之间传递信息。
const eventBus = Dz();
// 订阅事件
eventBus.on('sessionSwitch', (newSession) => {
console.log(`Switched to session ${newSession.sessionId}`);
});
// 发布事件
eventBus.emit('sessionSwitch', newSession);发布-订阅模式(Pub/Sub)的核心优势是解耦——事件的发布者不需要知道谁在监听,监听者也不需要知道谁在发布。这使得系统组件可以独立开发和演进。
Claude Code 的事件系统可能支持以下事件类型:
sessionSwitch:会话切换toolCall:工具调用toolResult:工具结果返回costUpdate:成本更新budgetWarning:预算警告error:错误发生
5.5.2 onSessionSwitch:会话切换的连锁反应
当会话切换时,系统需要协调多个组件的响应:
eventBus.on('sessionSwitch', async (newSession) => {
// 1. 更新工作目录
process.chdir(newSession.cwd);
// 2. 重置工具权限
G8.toolPermissionContext = newSession.permissions;
// 3. 更新 UI 状态
updateUI(newSession);
// 4. 恢复消息历史
loadMessageHistory(newSession.sessionId);
// 5. 更新日志上下文
logger.setSession(newSession.sessionId);
});会话切换是一个原子操作——要么全部切换成功,要么全部回滚。如果中间某个步骤失败(比如新会话的数据文件损坏),系统需要回滚到旧会话的状态。
5.5.3 c8() 结构化日志:会话的审计轨迹
c8() 是 Claude Code 的结构化日志函数。与简单的 console.log 不同,结构化日志以机器可读的格式记录事件,便于后续分析和审计。
c8('tool_execution', {
tool: 'Bash',
command: 'npm test',
sessionId: G8.sessionId,
timestamp: Date.now(),
result: 'success',
duration: 12500
});结构化日志的每个条目都包含:
- 事件类型:发生了什么(tool_execution, session_switch, api_call 等)
- 事件数据:事件的详细信息
- 会话标识:哪个会话产生的
- 时间戳:什么时候发生的
这些日志构成了完整的审计轨迹,可以回答"谁在什么时候做了什么、结果如何"这样的问题。在企业环境中,这种审计能力是安全合规的基础。
结构化日志的另一个重要用途是调试和性能分析。通过分析日志中的时间戳和持续时间,可以识别性能瓶颈——"哪个工具调用最慢?哪个会话消耗了最多的 token?"
5.6 状态管理的设计哲学总结
Claude Code 的状态管理系统展示了几个重要的设计原则:
5.6.1 单一真相源
所有的状态都集中在一个 G8 对象中,没有散落在各处的"野状态"。任何需要状态信息的组件都从 G8 获取,任何需要修改状态的操作都通过 G8 进行。这消除了状态不一致的可能性。
5.6.2 受控的可变性
状态不是"想改就改"的。不可变字段在创建后锁定,可变字段通过特定方法修改。每一次状态变更都可以被追踪和审计。
5.6.3 成本即状态
成本不是事后计算的——它是一个实时的、始终被追踪的状态维度。totalCostUSD 和 tokenCounter 的存在使得成本控制从"事后报告"变成了"实时调控"。
5.6.4 事件驱动的协调
组件之间不直接调用,而是通过事件系统通信。这使得系统可以灵活地添加新的响应者(新的监听器),而不影响已有的组件。
5.6.5 可恢复性
会话状态可以被序列化、持久化和恢复。这使得 Claude Code 不会因为意外中断而丢失所有工作进度。
状态管理是 Claude Code 最不"性感"但也最关键的子系统。用户看不到它,但每一次流畅的会话切换、每一次准确的成本报告、每一次无缝的工具调用,背后都有状态管理系统在默默工作。
如果说工具系统给了 Claude Code "行动"的能力,那么状态管理系统给了它"记忆"和"持续性"的能力。两者结合,使得 Claude Code 不只是一个"聪明的聊天机器人",而是一个可以持续协作的编程伙伴。
接下来,我们将讨论一个更加严肃的话题——安全。一个拥有强大能力的 AI,如何确保它不会做出有害的事情?Claude Code 的三层安全模型给出了答案。
第6章:权限即契约 — 安全模型的三层防御
6.0 章节导言
权力越大,责任越大。
当你赋予一个 AI 修改文件、执行命令、搜索代码的能力时,你同时也在赋予它犯错甚至造成破坏的能力。一个错误的 rm -rf 命令可能删除整个项目;一个不当的文件修改可能引入难以发现的 bug;一个越界的搜索可能泄露敏感信息。
Claude Code 的设计者深刻理解这一点。他们没有选择"要么信任、要么不用"的二元对立,而是构建了一个精细的三层安全模型,在能力与安全之间找到了平衡。
这一章,我们将逐层剖析这个安全模型,看看它是如何在不牺牲 AI 能力的前提下,确保每一次操作都在可控范围之内的。
6.1 第一层:Schema 层约束
6.1.1 类型定义即安全边界
这是最基础但也最容易被忽视的一层。还记得我们在第4章讨论的工具类型定义吗?它们不仅仅是编程便利——它们是第一道安全防线。
export interface FileWriteInput {
file_path: string; // 必须是绝对路径
content: string;
}file_path 是一个 string 类型——这意味着在 TypeScript 的类型系统层面,任何字符串都是合法的。但文档中明确要求"必须是绝对路径"。这种约束是在运行时强制执行的——即使类型系统允许你传递相对路径,运行时检查也会拒绝它。
这是一种防御式编程的实践:类型系统提供编译时的基本保障,运行时提供额外的验证。两者结合,形成了一个纵深防御体系。
6.1.2 绝对路径:消除路径歧义
为什么所有文件操作都要求绝对路径?
考虑这样的场景:AI 收到的指令是"修改 config 文件"。如果允许相对路径,AI 可能会尝试写入 config.json——但相对于什么?当前工作目录?Claude Code 的安装目录?用户的 home 目录?
绝对路径消除了这种歧义:
// ❌ 相对路径——可能指向任何地方
FileWrite({ file_path: "config.json", content: "..." })
// ✅ 绝对路径——明确无误
FileWrite({ file_path: "/Users/alice/project/config.json", content: "..." })但绝对路径本身也有安全风险——AI 可能会尝试写入 /etc/passwd 或 ~/.ssh/authorized_keys。这种风险由第二层和第三层防御来应对(权限模式和沙箱)。
6.1.3 输出限额:防止上下文爆炸
Schema 层约束不仅作用于输入,也作用于输出。但这里的"约束"不是类型层面的,而是协议层面的。
export interface GlobOutput {
filenames: string[];
truncated: boolean; // 结果是否被截断(限制100个)
}Glob 的结果默认最多返回 100 个文件。这不是技术限制(找到 101 个文件也不会崩溃),而是安全限制——防止 AI 的上下文窗口被海量数据淹没。
Grep 的 head_limit(默认 250)和 offset 也是同样的机制。它们不是在保护系统安全,而是在保护AI 的"注意力安全"——确保 AI 不会被无关信息淹没,从而做出错误的决策。
6.1.4 域名过滤:网络访问的白名单/黑名单
WebSearch 工具的域名过滤是 Schema 层约束的一个精彩案例:
export interface WebSearchInput {
query: string;
allowed_domains?: string[]; // 只在这些域名中搜索
blocked_domains?: string[]; // 不在这些域名中搜索
}这种设计允许在工具调用层面控制 AI 可以访问的网络资源。企业用户可以通过配置设置 blocked_domains,防止 AI 访问竞争对手的网站或已知的恶意网站。
但注意——这些是可选参数。如果不指定,AI 可以搜索整个互联网。域名过滤的安全效果取决于配置层面的设置,而不是工具本身的约束。这体现了三层防御的协作关系:Schema 层提供能力(支持域名过滤),配置层提供策略(设置具体的过滤规则)。
6.1.5 枚举类型:限制选择的范围
AskUserQuestion 工具的选项数量限制是 Schema 层约束的另一个例子:
questions: [
{
question: string;
options: [ /* 2-4个选项 */ ]; // @minItems 2, @maxItems 4
}
] // @minItems 1, @maxItems 4一个问题上最多 4 个选项,一次最多问 4 个问题。这些限制不是因为技术做不到更多,而是因为:
- 太多选项会增加用户的认知负担
- 太多问题会打断工作流
- 枚举限制确保了 UI 的一致性和可预测性
6.1.6 Schema 层的局限性
Schema 层约束是第一道防线,但它有自己的局限性:
- 类型是静态的,世界是动态的:
file_path: string无法在编译时区分"好的"路径和"坏的"路径。 - 可选参数可能被忽略:域名过滤、head_limit 等安全特性只有在被显式使用时才生效。
- 无法防止"合法但有害"的操作:删除一个用户明确指定的文件,从类型角度看是完全合法的。
这些局限性需要由第二层和第三层防御来弥补。
6.2 第二层:会话模式约束
6.2.1 六种权限模式
Claude Code 定义了六种权限模式,它们构成了一个从"极度保守"到"完全信任"的光谱:
type PermissionMode =
| "default" // 默认模式:读写操作需要确认,搜索操作自动执行
| "acceptEdits" // 接受编辑:文件修改自动通过,Bash 仍需确认
| "dontAsk" // 不询问:大多数操作自动执行,只阻止极度危险的
| "bypassPermissions" // 绕过权限:所有操作自动执行,无任何确认
| "plan" // 计划模式:AI 只能规划,不能执行
| "auto" // 自动模式:根据上下文自动判断让我们深入每一种模式:
default(默认模式)
这是最常用的模式,也是大多数用户首次使用 Claude Code 时的体验。它的核心原则是:
- 文件读取和搜索(FileRead、Grep、Glob):自动执行,无需确认。因为这些操作是只读的,不会造成破坏。
- 文件写入和编辑(FileWrite、FileEdit):需要用户确认。每次修改前,用户会看到一个 diff 预览,可以接受、拒绝或手动修改。
- 命令执行(Bash):需要用户确认。用户会看到命令描述和命令本身,可以选择执行或拒绝。
- 网络操作(WebSearch、WebFetch):可能需要确认,取决于配置。
default 模式的哲学是**"读自由、写受限"**——获取信息是安全的,修改世界需要谨慎。
acceptEdits(接受编辑模式)
FileEditOutput.userModified: booleanacceptEdits 模式将文件编辑的确认步骤去掉——AI 的文件修改会自动通过。但 Bash 命令仍然需要确认。
这个模式适合的场景是:你信任 AI 的文件编辑能力(通常确实做得不错),但仍然想对命令执行保持控制。
注意 FileEditOutput 中的 userModified 字段——即使在 acceptEdits 模式下,用户仍然可以手动介入修改 AI 的提议。这个字段记录了"用户是否修改了 AI 的提议",为审计提供信息。
dontAsk(不询问模式)
BashInput.dangerouslyDisableSandbox: booleandontAsk 模式进一步放宽了限制——大多数操作自动执行,只阻止极度危险的操作(比如删除系统关键文件、网络攻击等)。
这个模式适合的场景是:你正在进行一个自动化程度高的任务(比如批量重构),频繁的确认弹窗反而成为了障碍。但 dontAsk 不是 bypassPermissions——系统仍然保留最后的安全网。
bypassPermissions(绕过权限模式)
这是最"危险"的模式——所有操作自动执行,没有任何确认步骤。在这种模式下,AI 拥有完整的自主权。
// AgentInput 也支持模式设置
mode?: "acceptEdits" | "bypassPermissions" | "default" | "dontAsk" | "plan"注意 bypassPermissions 也可以作为子 Agent 的模式。当你创建一个子 Agent 来执行一个完全可信任的任务时,可以给它这个模式——但主 Agent 本身应该谨慎使用。
plan(计划模式)
plan 模式是六种模式中最特殊的——它不是简单地"放宽"或"收紧"权限,而是改变了操作的性质。在 plan 模式下,AI 只能规划操作,不能实际执行。
这种模式的核心价值在于人机协作的预审:
- AI 分析问题,制定计划
- 人类审查计划,提出修改
- 计划通过后,退出 plan 模式,执行计划
plan 模式与 ExitPlanMode 工具配合使用(我们稍后会详细讨论),构成了一个完整的"规划-审批-执行"工作流。
auto(自动模式)
auto 模式让系统根据上下文自动判断应该使用哪种模式。这是一种自适应的安全策略——系统会根据操作的类型、历史行为、用户偏好等因素动态调整安全级别。
比如,对于简单的文件读取操作,auto 模式可能等同于 default;对于你已经多次批准的同类操作,auto 模式可能自动升级到 acceptEdits。
6.2.2 toolPermissionContext:细粒度的权限状态
toolPermissionContext: {
"Bash": "ask", // Bash 命令需要确认
"FileEdit": "auto", // 文件编辑自动执行
"FileWrite": "ask", // 文件写入需要确认
"WebSearch": "auto", // 搜索自动执行
// ...
}toolPermissionContext 为每个工具维护独立的权限状态。这意味着你可以对不同的工具设置不同的权限级别——FileEdit 自动通过,但 Bash 仍然需要确认。
这种细粒度的权限控制比统一的权限模式更加灵活。但它也带来了管理复杂度——用户需要理解每个工具的安全含义,才能做出明智的配置。
在实际使用中,权限模式和 toolPermissionContext 的关系可能是这样的:
- 权限模式设定了一个"基线"安全级别
- toolPermissionContext允许在基线上对特定工具进行微调
- 用户的实时操作(比如点击"总是允许此类操作")可以进一步修改 toolPermissionContext
6.2.3 权限模式的设计哲学
Claude Code 的六种权限模式体现了渐进式信任的设计哲学:
plan → default → acceptEdits → dontAsk → bypassPermissions
↑ ↑
极度保守 完全信任这个光谱不是随意的——每一步都移除了一个安全关卡:
- plan → default:从"只规划"到"可执行"(但需要确认)
- default → acceptEdits:从"修改需确认"到"修改自动通过"
- acceptEdits → dontAsk:从"命令需确认"到"命令自动通过"
- dontAsk → bypassPermissions:从"系统保留安全网"到"完全自主"
用户可以根据自己的信任程度和任务的风险级别,在这个光谱上选择合适的位置。而且这个选择不是永久的——你可以在一个会话中切换模式,或者为不同的子 Agent 设置不同的模式。
6.3 第三层:执行层沙箱
6.3.1 沙箱的本质
如果 Schema 层约束是"门锁",权限模式是"门卫",那么沙箱就是"金库"。
门锁防止错误进入,门卫检查身份,但即使有人通过了门锁和门卫,金库仍然限制了他们能拿走的东西。
Claude Code 的 Bash 沙箱就是这样——即使 AI 决定执行一个命令(通过了 Schema 约束),用户批准了这个命令(通过了权限检查),沙箱仍然限制了命令能做什么。
沙箱通常限制以下资源:
- 文件系统:限制可访问的目录。通常只允许访问项目目录和临时目录,不允许访问系统目录(
/etc、/usr)或用户的私有目录。 - 网络:限制网络访问。可能阻止访问某些端口或域名。
- 环境变量:过滤敏感的环境变量(比如 API 密钥、密码)。
- 系统调用:限制某些危险的系统调用(比如
fork、exec、mount)。
6.3.2 dangerouslyDisableSandbox:危险的开关
dangerouslyDisableSandbox?: boolean;我们已经讨论过这个参数。但在这里,让我们从安全架构的角度重新审视它。
dangerouslyDisableSandbox 的存在本身就说明了一个事实:沙箱不是完美的。有些合法的操作确实需要绕过沙箱。如果沙箱是完美的,就不需要这个开关了。
这个开关的设计哲学是:
- 默认安全:沙箱默认启用,用户不需要做任何事就能获得安全保护。
- 显式冒险:如果需要绕过沙箱,必须显式设置
dangerouslyDisableSandbox: true——这个长而吓人的名字本身就是一个安全提示。 - 可审计:绕过沙箱的操作会被记录在输出和日志中。
这三条原则构成了一个安全默认+显式覆盖的模式——这是安全工程中的一个经典模式,从 Unix 的 sudo 到 Kubernetes 的 RBAC,都可以看到同样的思想。
6.3.3 企业策略:bq6 过滤器
在企业环境中,安全策略由组织管理员统一管理。Claude Code 支持企业策略覆盖(Enterprise Policy Override)——管理员可以设置组织级别的安全策略,这些策略优先于用户的个人设置。
// 企业策略的伪代码表示
enterprisePolicy: {
bq6: {
allowedTools: ["FileRead", "Grep", "Glob", "WebSearch"],
blockedTools: ["Bash", "FileWrite", "FileEdit"],
allowedSettings: ["theme", "model"],
blockedSettings: ["maxBudgetUsd", "permissionMode"],
// ...
}
}bq6 是企业策略过滤器的内部名称。它通过两个机制来控制安全:
- 工具白名单/黑名单:
allowedTools指定允许使用的工具,blockedTools指定禁止使用的工具。两者可以同时使用——白名单中的工具始终允许,黑名单中的工具始终禁止,其余工具遵循个人设置。 - 配置白名单/黑名单:
allowedSettings和blockedSettings控制用户可以修改哪些配置。比如,企业可能禁止用户关闭沙箱或更改权限模式。
这种设计确保了即使在最宽松的个人设置下,企业策略仍然能维持一个安全底线。个人自由在组织安全面前让步。
企业策略还可以控制配置的来源——allowedSettingSources 限制了用户可以从哪些来源获取配置。比如,企业可能只允许从组织配置服务器获取配置,禁止用户手动修改 ~/.claude/settings.json。
6.4 计划模式深度分析
6.4.1 ExitPlanMode:从思考到行动的转换器
计划模式(plan mode)是 Claude Code 安全模型中最具创新性的设计之一。它不是一个简单的"开/关"开关,而是一个结构化的工作流——一个从"思考"到"行动"的受控转换过程。
export interface ExitPlanModeInput {
allowedPrompts?: {
tool: "Bash"; // 目前只支持 Bash 工具
prompt: string; // 语义描述
}[];
}ExitPlanMode 的输入中,allowedPrompts 是最关键的字段。它不是授权具体的命令,而是授权语义类别的命令。
// ❌ 具体命令授权(不安全,因为命令可能被微调)
allowedPrompts: [{ tool: "Bash", prompt: "npm install" }]
// ✅ 语义类别授权(更安全,涵盖了所有相关的命令)
allowedPrompts: [{ tool: "Bash", prompt: "install dependencies" }]"install dependencies" 这个语义描述覆盖了 npm install、yarn install、pip install 等所有属于"安装依赖"这一类别的命令。这是一种意图级别的授权——你授权的不是"做什么",而是"做什么类型的事"。
6.4.2 allowedPrompts:语义授权的威力
语义授权的设计理念是深刻而优雅的。它认识到:
命令是无限的,意图是有限的:命令的具体形式有无数种(
npm install react、npm install react@18、yarn add react...),但它们表达的意图可以归纳为少数几类(安装依赖、运行测试、构建项目...)。人类思考的是意图,不是命令:当用户审查一个计划时,他们想要确认的是"AI 想要安装依赖,这合理吗?",而不是"AI 想要运行
npm install --save react@18.2.0,这安全吗?"。语义授权更具鲁棒性:即使用户微调了计划中的某个命令(比如从
npm install改为yarn install),只要它仍然属于授权的语义类别,就不需要重新审批。
这种设计也带来了一个有趣的挑战:如何准确匹配命令到语义类别。系统需要一个"命令→语义"的映射机制。这可能基于规则(正则匹配)、基于学习(模型推理)或基于混合方案。
6.4.3 计划模式的完整工作流
1. 进入 plan 模式
└── AI 只能分析和规划,不能执行任何修改操作
2. AI 制定计划
└── 生成一个结构化的操作计划
└── 计划中包含 allowedPrompts 语义授权
3. 用户审查计划
└── 可以修改、拒绝或批准计划
└── planWasEdited 标记记录用户是否修改了计划
4. 调用 ExitPlanMode
└── 退出 plan 模式,进入执行阶段
└── allowedPrompts 生效
5. 执行计划
└── AI 按照计划执行操作
└── Bash 命令根据 allowedPrompts 自动或半自动执行ExitPlanMode 的输出类型揭示了更多细节:
export interface ExitPlanModeOutput {
plan: string | null; // 最终的计划内容
isAgent: boolean; // 是否为 Agent 上下文
filePath?: string; // 计划保存的文件路径
hasTaskTool?: boolean; // 当前上下文是否有 Agent 工具
planWasEdited?: boolean; // 用户是否修改了计划
awaitingLeaderApproval?: boolean; // 是否等待团队领导审批
requestId?: string; // 审批请求ID
}几个关键字段的分析:
planWasEdited:这个字段记录了用户是否在审批过程中修改了计划。如果用户修改了计划,AI 在执行时应该使用修改后的版本,而不是原始版本。awaitingLeaderApproval和requestId:这两个字段揭示了 Agent 团队协作中的一个场景——子 Agent 制定的计划需要发送给主 Agent(团队领导)审批。这形成了一个多层次的审批链。hasTaskTool:这个字段指示当前上下文是否可以使用 Agent 工具。在某些受限环境中(比如 plan 模式本身或特定的沙箱配置),Agent 工具可能不可用。
6.4.4 Worktree 隔离:物理层面的安全网
export interface EnterWorktreeInput {
name?: string; // worktree 名称(可选,自动生成)
}
export interface ExitWorktreeInput {
action: "keep" | "remove"; // 保留或删除 worktree
discard_changes?: boolean; // 确认丢弃未提交的更改
}Git Worktree 是 Git 的一个功能——它允许你在同一个仓库中同时签出多个分支到不同的目录。Claude Code 将这个功能用作安全机制:
- 进入 Worktree:创建一个隔离的代码副本,AI 在副本中工作。
- 执行操作:AI 在副本中自由修改代码,不会影响主工作区。
- 审查结果:用户检查副本中的修改。
- 退出 Worktree:
keep:保留 worktree 和新分支(修改不丢失,但也不影响主分支)remove:删除 worktree 和分支(如果确认discard_changes)
这是一种物理隔离——不是通过规则限制 AI 能做什么,而是通过环境隔离确保 AI 的操作不会影响真实的工作区。即使 AI 在 worktree 中执行了灾难性的操作,主工作区也毫发无损。
discard_changes?: boolean 的设计值得特别关注。当 action: "remove" 时,如果 worktree 中有未提交的更改,系统会拒绝删除并列出这些更改。只有当用户显式设置 discard_changes: true 时,删除才会执行。
这是双重确认模式的一个经典实现——不是通过弹出对话框,而是通过类型系统强制要求额外的确认参数。这种方式既安全又不打扰用户(因为确认是通过参数传递的,而不是在操作过程中打断用户)。
6.5 安全模型的设计哲学总结
Claude Code 的三层安全模型可以被总结为以下原则:
6.5.1 纵深防御(Defense in Depth)
没有一层是完美的,但三层叠加后,安全漏洞的概率大幅降低:
- Schema 层可能遗漏某些输入验证 → 权限模式提供了第二道检查
- 权限模式可能被配置不当 → 沙箱提供了最后的物理隔离
- 沙箱可能被绕过(dangerouslyDisableSandbox)→ 但这需要显式的用户确认和审计记录
6.5.2 安全默认,显式冒险
几乎所有安全相关的默认值都是"安全"的:
- 沙箱默认启用
- 权限模式默认为 default(最保守的可执行模式)
- 输出限额默认启用
- Worktree 隔离在危险操作时建议使用
要"冒险",必须显式声明——通过参数、通过配置、通过确认。
6.5.3 渐进式信任
六种权限模式提供了一个平滑的信任光谱,而不是二元的选择。用户可以根据自己的经验和对 AI 的信任程度,在这个光谱上找到合适的位置。
6.5.4 审计即安全
每一次安全相关的决策都被记录:
- 工具调用的 description
- dangerouslyDisableSandbox 的标记
- 退出 plan 模式时的 planWasEdited
- 企业策略的 bq6 过滤记录
这些审计记录不是为了"事后追责",而是为了"事前威慑"——当所有操作都有迹可循时,恶意或鲁莽的行为就会被自然地遏制。
6.5.5 人的最终控制权
无论安全模型多么精细,最终的控制权始终在人类手中:
- 权限模式由用户选择
- 计划由用户审批
- 危险操作由用户确认
- 企业策略由管理员制定
AI 不是独立行动者——它是一个在人类授权范围内行动的代理人。Claude Code 的安全模型确保了这个代理人不会越界。