Skip to content
源码分析手册

CwdChanged / FileChanged:会话环境的响应式补丁点

在 Claude Code 的运行时模型中,工作目录(CWD)的变化和关键文件的更新直接影响着后续工具执行的环境。CwdChangedFileChanged 这组钩子确保了会话能够实时响应这些变化。

本质

这组钩子本质上是 会话环境的动态刷新机制(Session Env Refresher)

它们不是一个通用的“全项目文件监视器”,因为它们只在特定的配置文件发生变化或目录切换时触发。其核心目的不是“通知用户”,而是“为后续的 Bash 命令重新准备环境”。

  • CwdChanged:在目录切换时重置并重建环境。
  • FileChanged:在被关注的文件(如 .env)变化时增量刷新环境。

实现机制

底座逻辑位于 claude-code-opensource/src/utils/hooks/fileChangedWatcher.ts

  1. Watcher 的懒启动:系统不会无条件启动文件监听。只有当配置中显式存在 FileChangedCwdChanged 时,才会通过 chokidar 启动监听。
  2. Matcher 的特殊语义:对于 FileChangedmatcher 字段的行为与其他 Hook 不同。它不是通用的正则匹配,而是在 resolveWatchPaths 中被解析为当前目录下要监听的文件名列表(如 .envrc|.env)。
  3. CWD 变化的权威来源CwdChanged 的触发源不是 UI 层,而是 claude-code-opensource/src/utils/Shell.ts。每当 Bash 命令执行完毕,系统都会读取 pwd -P。如果发现物理路径变化,会立即清除旧的环境缓存并异步触发 onCwdChangedForHooks
  4. 环境变量文件注入(CLAUDE_ENV_FILE):这是这组钩子的最强杀招。Hook 脚本可以通过写入由环境变量 CLAUDE_ENV_FILE 指定的临时 .sh 文件,来注入后续所有 Bash 工具可见的环境变量。

边界条件

  • 监听列表的排他性:系统只监听 matcher 中定义的文件和 Hook 动态返回的 watchPaths。它不会监听全量源代码。
  • Shell 命令驱动CwdChanged 仅能捕获由 Claude 内部 Bash 工具执行导致的目录变化。如果你在外部终端 cd,Claude 本进程无法直接感知。
  • 环境覆盖顺序:通过 FileChanged 注入的环境变量具有极高的优先级,它们会按固定顺序拼接到后续 Bash 进程的启动脚本中。
  • Watcher 重启开销:每次 CwdChanged 触发都可能导致监听器的重启。因此,应避免在 watchPaths 中添加过于庞大的目录树。

延伸阅读

源码锚点

  • claude-code-opensource/src/utils/hooks/fileChangedWatcher.tsCwdChanged / FileChanged 的核心运行时逻辑。
📄 src/utils/hooks/fileChangedWatcher.ts — `CwdChanged` / `FileChanged` 的核心运行时逻辑。L7-11 of 192
typescript
  executeCwdChangedHooks,
  executeFileChangedHooks,
  type HookOutsideReplResult,
} from '../hooks.js'
import { clearCwdEnvFiles } from '../sessionEnvironment.js'
  • claude-code-opensource/src/utils/Shell.ts — 真实 CWD 变化的来源捕获。
📄 src/utils/Shell.ts — 真实 CWD 变化的来源捕获。L221-238 of 475
typescript
  // This can happen when a command deletes its own CWD (e.g., temp dir cleanup).
  try {
    await realpath(cwd)
  } catch {
    const fallback = getOriginalCwd()
    logForDebugging(
      `Shell CWD "${cwd}" no longer exists, recovering to "${fallback}"`,
    )
    try {
      await realpath(fallback)
      setCwdState(fallback)
      cwd = fallback
    } catch {
      return createFailedCommand(
        `Working directory "${cwd}" no longer exists. Please restart Claude from an existing directory.`,
      )
    }
  }
  • claude-code-opensource/src/utils/sessionEnvironment.tsCLAUDE_ENV_FILE 环境脚本的清理与拼接实现。
📄 src/utils/sessionEnvironment.ts — `CLAUDE_ENV_FILE` 环境脚本的清理与拼接实现。L72-90 of 167
typescript
  // Check for CLAUDE_ENV_FILE passed from parent process (e.g., HFI trajectory runner)
  // This allows venv/conda activation to persist across shell commands
  const envFile = process.env.CLAUDE_ENV_FILE
  if (envFile) {
    try {
      const envScript = (await readFile(envFile, 'utf8')).trim()
      if (envScript) {
        scripts.push(envScript)
        logForDebugging(
          `Session environment loaded from CLAUDE_ENV_FILE: ${envFile} (${envScript.length} chars)`,
        )
      }
    } catch (e: unknown) {
      const code = getErrnoCode(e)
      if (code !== 'ENOENT') {
        logForDebugging(`Failed to read CLAUDE_ENV_FILE: ${errorMessage(e)}`)
      }
    }
  }

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