niuzj

Claude Code Plan Mode:权限边界与审批机制

Plan Mode 最容易被误解为一段“先分析、再执行”的提示词。这个解释描述了模型表面的行为,却回避了一个更重要的工程问题:如果模型在规划阶段仍然发起了写文件或执行命令的 Tool Call,谁负责阻止它?

答案不能只是另一段 Prompt。Prompt 可以约束模型的倾向,不能构成可验证的权限边界。Claude Code 的 Plan Mode 本质上是 Agent Runtime 中的一种权限模式:它在原有 Agent Loop 之上叠加会话状态、运行时提醒、本地权限检查、Plan 文件和人工审批链路。

本文把证据分成两类。用户可见的行为以 Claude Code 官方 Plan Mode 文档 为准;消息构造、内部状态和审批后的上下文转换,则来自本机 Claude Code 2.1.207 源码快照,对应构建提交 bc512d56332530b2be3f5079e29ec17aa20b8553。后者是特定版本的逆向观察,不应视为稳定 API。

Plan Mode 没有替换 Agent Loop

进入 Plan Mode 后,Claude Code 仍然运行同一套模型与工具循环:

构造模型请求
→ 模型返回文本或 tool_use
→ Claude Code 在本地处理工具调用
→ tool_result 回到对话
→ 模型继续推理,或结束当前轮次

变化发生在循环外围。可以把它拆成六个相互独立的部分:

Plan Mode
= 会话模式状态
+ 持续的规划指令
+ 工具执行前的权限检查
+ 独立的 Plan 文件
+ 用户审批边界
+ 审批后的上下文转换

这也解释了 Plan Mode 与 ReAct 类循环的关系:ReAct 描述 Agent 如何在推理和动作之间迭代,Plan Mode 描述这套循环当前处于什么权限状态,以及何时允许从调查进入有副作用的执行。

Claude Code Plan Mode 由提示、状态、权限检查、Plan 文件和用户审批共同构成

plan 首先是客户端状态

Anthropic Messages API 并没有一个可直接开启 Plan Mode 的字段:

{
  "mode": "plan"
}

这样的请求参数并不存在。根据 2.1.207 源码快照,模式状态由 Claude Code 客户端维护,核心状态可以等价表示为:

mode = "plan"
prePlanMode = previousMode

mode 参与后续 Prompt、工具和权限决策;prePlanMode 记录进入规划前的模式,并参与退出与恢复逻辑。它不是最终权限模式的唯一来源:计划获批后进入 Manual、Accept Edits 或其他可用模式,仍以用户在批准界面中的选择为准。

官方文档确认了三种常见入口:

  • 在交互会话中按 Shift+Tab 切换到 Plan;
  • /plan 为一次请求进入规划;
  • 启动时传入 claude --permission-mode plan

官方文档还明确说明,再按一次 Shift+Tab 可以直接离开 Plan Mode,不需要批准当前计划。因此,ExitPlanMode 是 Agent 提交计划并请求批准的主要路径,但不是离开 Plan Mode 的唯一出口。

源码快照中还存在 EnterPlanMode 工具路径,使模型可以提出进入规划模式的请求。这里出现的是“是否切换到特殊模式”的确认界面:用户批准后改变整个会话的权限状态。它不同于普通 Permission Prompt,后者只是在当前模式下判断某一次工具调用是否允许执行。无论入口来自界面、命令行还是工具调用,真正改变行为的是本地 Runtime 状态,而不是模型自行宣告“现在开始规划”。

Prompt 负责引导,权限系统负责兜底

Plan Mode 仍然需要 Prompt。模型必须知道当前目标是调查代码、澄清约束和形成方案,而不是立即修改项目。

2.1.207 源码快照中,这部分指令先被构造成内部 Attachment Message,发送 API 前再转换为一段 role: "user"<system-reminder>。等价的请求片段如下:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "<system-reminder>Plan mode is active...</system-reminder>"
    }
  ]
}

它通常被合并到最近一轮用户消息附近,但不保证始终是一条独立的最后消息。Todo、Hook、MCP 和其他运行时附件也会参与消息合并。

完整提醒不会在每一次模型调用中原样重复。源码快照显示,首次进入时会注入较完整的规划说明,后续轮次使用较短提醒;上下文压缩后还会重新注入模式信息,避免压缩结果丢失当前状态。

这套提醒能降低模型误操作的概率,但不能保证模型永远不生成写操作。因此,Plan Mode 的硬边界必须放在 Tool Call 真正执行之前:

模型生成 tool_use
→ 校验工具参数
→ Permission Pipeline 检查 mode、路径和规则
→ 允许执行 / 请求批准 / 拒绝执行

模型是否遵循 Prompt,和客户端是否允许动作,是两套不同的判断。前者属于行为引导,后者才是执行控制。

工具对模型可见,不代表工具一定可执行

一个常见推断是:Plan Mode 会从 API 的 tools[] 中删除所有写工具。源码快照显示,实际实现没有这么简单。

Claude Code 会在每轮请求前动态组装工具集合。部分具备写能力的工具仍可能出现在模型可见的 Tool Schema 中,因为规划阶段本身需要更新 Plan 文件。工具是否最终执行,要到本地 Permission Pipeline 中结合当前模式和目标路径判断。

因此需要区分两层能力:

层级 回答的问题
API Tool Schema 模型可以尝试调用什么
本地 Permission Pipeline 这一次调用是否允许执行

官方文档对用户可见行为的描述是:Plan Mode 可以读取文件、使用命令调查代码并生成计划,但不会修改源文件;相关权限提示仍按 Manual 模式处理。内部实现如何筛选工具、匹配路径和生成拒绝结果可能随版本变化,但“执行权限由客户端控制”是理解这一模式的关键。

Plan 文件是受控的写入例外

Plan Mode 不是绝对只读。对项目源码而言,Plan 文件是规划阶段最主要的写入例外;Claude Code Runtime 仍可能在自己的内部工作目录维护少量会话状态、缓存或临时产物。

2.1.207 源码快照中,Plan 默认保存到:

~/.claude/plans/<随机单词-slug>.md

例如:

~/.claude/plans/calm-silver-harbor.md

plansDirectory 还可以把目录改到其他位置。源码快照中,相对目录会以项目根目录为基准解析;配置无效或目标不可用时,路径逻辑会回退到默认 Plan 目录。当前会话对应的 Plan 文件在权限系统中有专门的放行路径,而普通项目文件仍受 Plan Mode 限制。

这个设计比“屏蔽全部 Write”多了一层语义:权限检查不仅要知道调用了什么工具,还要知道它准备写到哪里、写入对象是否属于当前会话允许的规划产物。Plan 文件由此成为调查阶段与执行阶段之间可审阅、可编辑的交接物。

这里描述的是 Claude Code 工具与权限管线中的语义边界,不是操作系统级的“只允许写一个路径”沙箱。把 Plan 文件称为写入例外,是针对项目源码修改而言;不能据此推断进程在文件系统层面只有这一条可写路径。

ExitPlanMode 建立了人工审批边界

计划写完并不意味着 Agent 可以自动开始修改代码。源码快照中的主流程会调用 ExitPlanMode,由 Claude Code 展示计划并等待用户决定。

官方文档确认,用户可以在这里:

  • 批准计划,并选择后续的权限模式;
  • 带着反馈继续规划;
  • Ctrl+G 在编辑器中直接修改计划;
  • 在开启 showClearContextOnPlanAccept 后,批准时选择清空规划上下文再执行。

批准动作同时完成两件事:退出 plan,并把会话切换到用户选择的可执行权限模式。它不是一句普通的“好的,请继续”,而是一次由客户端处理的状态转换。

根据 2.1.207 源码快照,审批后默认保留规划上下文。只有显式开启 showClearContextOnPlanAccept 时,批准界面才会提供清空上下文的可选路径;该设置默认值为 false

保留规划上下文

ExitPlanMode 的结果作为 tool_result 返回当前对话,其中包含已批准的 Plan。消息结构可以等价表示为:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_exit_plan",
      "content": "User has approved your plan...\n\n## Approved Plan:\n<完整计划>"
    }
  ]
}

主 Agent 保留调查阶段的文件、约束和决策信息,在同一条对话中进入执行阶段。

清空规划上下文

如果 showClearContextOnPlanAccept 已开启,且用户在批准时选择先清空上下文,Claude Code 会用完整计划创建新的执行请求。其核心形式是:

Implement the following plan:

<完整计划>

这条路径减少了大量搜索结果、临时判断和中间对话对上下文窗口的占用。代价也很明确:执行阶段主要依赖最终 Plan,规划过程中没有写入 Plan 的细节不再自然保留。

两条路径都没有引入一个专门解析 Markdown 的“Plan 执行器”。真正执行修改的仍然是主 Agent:它读取批准后的计划,重新进入普通工具循环,自主决定读取文件、编辑代码、运行测试和处理失败。

Subagent 是条件性的规划策略

源码快照中的典型规划指令会引导主 Agent 调查代码,并在任务适合拆分时使用 Explore 或 Plan 类型的 Subagent。这是条件性的编排策略,不是每次进入 Plan Mode 都必须执行的固定阶段。这里还需要区分两个不同层级的概念:

组件 职责
主线程 Plan Mode 控制整个会话的权限状态与审批边界
Explore Subagent 收集代码和上下文证据
Plan Subagent 从只读视角提出实施方案
Plan 文件 保存主 Agent 整理后的最终方案
ExitPlanMode 把最终方案送入用户审批

Subagent 可以参与调查和设计,但不能替代主线程的权限状态。主 Agent 仍负责核对关键文件、处理冲突信息、向用户澄清影响方案的问题,并把最终决策写入 Plan 文件。

这部分属于内部规划 Prompt 的版本化编排策略,不是官方文档承诺的固定阶段。版本升级可能调整是否启用 Subagent、Subagent 类型、调用顺序或 Prompt 文案,而 Plan Mode 的权限与审批边界仍可以保持不变。

Plan 与 Task 记录解决不同问题

Plan 文件也不是自动执行的任务清单。完成一次 Edit,不会反向修改 Markdown 中的复选框;某一步执行失败,也不会由 Plan 文件自身驱动重试。

两者的职责边界是:

记录 负责什么
Plan 描述准备实施的方案、范围和验证方式
Task / Todo 记录执行阶段当前做到哪里

执行阶段可以把批准后的 Plan 拆成任务,并通过 TaskCreateTaskUpdate 等运行时能力维护状态:

读取 Approved Plan
→ 创建执行任务
→ 标记 in_progress
→ 修改与验证
→ 标记 completed

任务是否完成,仍由 Agent 根据工具结果和验证结果判断。Runtime 保存状态并刷新界面,但不会因为 Plan 中写了一个步骤就自动证明它已经完成。

实现 Plan Mode 需要哪些运行时不变量

如果在其他 Agent 系统中实现类似能力,真正需要复用的不是 Claude Code 某一版 Prompt 文案,而是几条可以由 Runtime 保证的不变量:

  1. 规划状态必须由客户端保存,不能只存在于模型输出中。
  2. 有副作用的工具调用必须在执行前经过模式与权限检查。
  3. 规划产物需要独立存储,并成为用户可以审阅的明确对象。
  4. 未经用户批准,规划阶段不能自行跨入执行阶段。
  5. 批准必须对应真实的权限状态转换,并定义上下文保留策略。
  6. Prompt 可以改善模型行为,但不能替代上述任何硬约束。

只增加“请先制定计划,不要修改文件”这类指令,得到的是规划风格,而不是完整的 Plan Mode。判断边界是否成立,不应看模型能否写出结构清晰的步骤,而应看一次未经批准的 Tool Call 能否越过 Runtime。

版本与源码线索

公开行为参考 Claude Code 官方 Permission Modes 文档。文中的内部实现结论来自本机 Claude Code 2.1.207 源码快照,构建提交为 bc512d56332530b2be3f5079e29ec17aa20b8553,主要涉及以下路径:

src/commands/plan/plan.tsx
src/tools/EnterPlanModeTool/EnterPlanModeTool.ts
src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts
src/utils/messages.ts
src/utils/attachments.ts
src/utils/plans.ts
src/utils/permissions/filesystem.ts
src/services/api/claude.ts

这些文件名、内部类型和消息拼装方式都可能在后续版本变化。对外可依赖的是官方文档描述的权限行为;源码分析用于解释当前版本如何把这些行为落实为状态、消息和审批链路。