Skip to content
源码分析手册

Plan 模式:带计划文件的逻辑隔离预演

在处理复杂的代码库重构或多步骤逻辑修改时,人类开发者往往会先打草稿。Claude Code 的 Plan 模式(计划模式)正是为了将这一行为标准化、工程化而设计的特殊运行状态。它是一套强制性的“思考-审批-执行”工作流,而非简单的“只读模式”。

一句话讲清楚

Plan 模式本质上是 推理能力与副作用操作的逻辑隔离层

  • 它不是什么:它不是简单的“只读浏览模式”。它要求模型在 plan.md 中物化其思维链(Chain of Thought),并在进入实际修改阶段前向人类递交“施工图纸”。
  • 它的核心理念:通过强迫模型将复杂目标分解为可审计的子任务,并在一个绝对安全的“逻辑沙箱”内完成所有的探索性读取工作。

实现机制

Plan 模式的驱动核心位于 claude-code-opensource/src/tools/EnterPlanModeTool/claude-code-opensource/src/utils/plans.ts

1. 状态切换(State Transition)

当模型判定当前任务需要深度构思并调用 EnterPlanModeTool 时,AppState 中的 permissionMode 会被切换为 plan。这一切换会立即触发全局的权限短路。

2. 权限短路(The Logical Circuit Breaker)

plan 模式下,权限引擎 canUseTool(位于 claude-code-opensource/src/utils/permissions/permissions.ts)会应用最严格的过滤规则:

  • 只读放行:允许所有的读取(FileRead)、搜索(Grep/Glob)和上下文探知工具。
  • 写操作拦截:任何试图修改文件(FileWrite/Edit)或执行 Bash 命令的尝试都会被权限引擎预先拦截。模型会收到提示:“当前处于计划阶段,严禁执行修改,请先完善 plan.md”。这种拦截甚至不经过用户询问,直接由系统在逻辑层执行“静默拒绝”。

3. 计划物化与思维对齐

系统会通过 src/utils/plans.ts 在当前工作区创建一个显式的 plan.md 文件。

  • 实时同步:模型被鼓励(通过系统指令)在探索过程中不断回填和修正 plan.md
  • CoT 效应:将计划写入文件有两个目的:供用户审阅,同时让模型自己维持一个长期的任务状态栈,减少在复杂重构中的逻辑发散。

4. 受控退出(The Exit Handshake)

退出 Plan 模式是一个必须由模型显式发起或人类确认的“握手协议”。通过 ExitPlanModeTool,系统会:

  • 最终审计:强制用户最后一次审阅 plan.md 的内容。
  • 环境对齐:询问用户是否需要将当前的计划内容合并到主对话流中,或者作为执行阶段的元数据保留。
  • 权限复原:只有当握手完成后,permissionMode 才会恢复到 default 或用户指定的其他状态。

限制与陷阱

  • 读写绝对性:在 plan 模式下,连 BashTool 的只读命令(如 ls)有时也会受限,如果该命令可能产生未预料的副作用,系统会倾向于保守地要求模型使用原生的 Node.js 读取工具。
  • 非永久性文件plan.md 通常是临时性的,其主要目的是驱动当前的推理流。如果你需要长期保留方案,应在退出前明确告知模型将其转录为正式的项目文档。
  • 上下文继承:在 Plan 模式中读取的所有文件、搜索到的所有符号定义,在切换回执行模式后依然保留在模型的短期上下文(Transcript)中。这种无缝衔接保证了“想清楚”后能立即“写得准”。
  • 手动干预优先级:如果用户在命令行中手动强制退出(如使用 Ctrl+C 或特定命令),系统会回退到上一个安全状态,但不保证 plan.md 的最终一致性。

推荐阅读路径

源码锚点

📄 src/utils/permissions/PermissionMode.ts — `plan` 模式的状态枚举与全局配置定义。L52-58 of 142
typescript
  plan: {
    title: 'Plan Mode',
    shortTitle: 'Plan',
    symbol: PAUSE_ICON,
    color: 'planMode',
    external: 'plan',
  },
📄 src/utils/plans.ts — `plan.md` 文件的生命周期管理(生成、读写、合并逻辑)。L25-49 of 398
typescript
const MAX_SLUG_RETRIES = 10

/**
 * Get or generate a word slug for the current session's plan.
 * The slug is generated lazily on first access and cached for the session.
 * If a plan file with the generated slug already exists, retries up to 10 times.
 */
export function getPlanSlug(sessionId?: SessionId): string {
  const id = sessionId ?? getSessionId()
  const cache = getPlanSlugCache()
  let slug = cache.get(id)
  if (!slug) {
    const plansDir = getPlansDirectory()
    // Try to find a unique slug that doesn't conflict with existing files
    for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
      slug = generateWordSlug()
      const filePath = join(plansDir, `${slug}.md`)
      if (!getFsImplementation().existsSync(filePath)) {
        break
      }
    }
    cache.set(id, slug!)
  }
  return slug!
}

基于 Claude Code v2.1.88 开源快照的深度分析