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 描述这套循环当前处于什么权限状态,以及何时允许从调查进入有副作用的执行。

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 拆成任务,并通过 TaskCreate、TaskUpdate 等运行时能力维护状态:
读取 Approved Plan
→ 创建执行任务
→ 标记 in_progress
→ 修改与验证
→ 标记 completed
任务是否完成,仍由 Agent 根据工具结果和验证结果判断。Runtime 保存状态并刷新界面,但不会因为 Plan 中写了一个步骤就自动证明它已经完成。
实现 Plan Mode 需要哪些运行时不变量
如果在其他 Agent 系统中实现类似能力,真正需要复用的不是 Claude Code 某一版 Prompt 文案,而是几条可以由 Runtime 保证的不变量:
- 规划状态必须由客户端保存,不能只存在于模型输出中。
- 有副作用的工具调用必须在执行前经过模式与权限检查。
- 规划产物需要独立存储,并成为用户可以审阅的明确对象。
- 未经用户批准,规划阶段不能自行跨入执行阶段。
- 批准必须对应真实的权限状态转换,并定义上下文保留策略。
- 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
这些文件名、内部类型和消息拼装方式都可能在后续版本变化。对外可依赖的是官方文档描述的权限行为;源码分析用于解释当前版本如何把这些行为落实为状态、消息和审批链路。