CwdChanged / FileChanged:会话环境的响应式补丁点
在 Claude Code 的运行时模型中,工作目录(CWD)的变化和关键文件的更新直接影响着后续工具执行的环境。CwdChanged 和 FileChanged 这组钩子确保了会话能够实时响应这些变化。
本质
这组钩子本质上是 会话环境的动态刷新机制(Session Env Refresher)。
它们不是一个通用的“全项目文件监视器”,因为它们只在特定的配置文件发生变化或目录切换时触发。其核心目的不是“通知用户”,而是“为后续的 Bash 命令重新准备环境”。
CwdChanged:在目录切换时重置并重建环境。FileChanged:在被关注的文件(如.env)变化时增量刷新环境。
实现机制
底座逻辑位于 claude-code-opensource/src/utils/hooks/fileChangedWatcher.ts:
- Watcher 的懒启动:系统不会无条件启动文件监听。只有当配置中显式存在
FileChanged或CwdChanged时,才会通过chokidar启动监听。 - Matcher 的特殊语义:对于
FileChanged,matcher字段的行为与其他 Hook 不同。它不是通用的正则匹配,而是在resolveWatchPaths中被解析为当前目录下要监听的文件名列表(如.envrc|.env)。 - CWD 变化的权威来源:
CwdChanged的触发源不是 UI 层,而是claude-code-opensource/src/utils/Shell.ts。每当 Bash 命令执行完毕,系统都会读取pwd -P。如果发现物理路径变化,会立即清除旧的环境缓存并异步触发onCwdChangedForHooks。 - 环境变量文件注入(CLAUDE_ENV_FILE):这是这组钩子的最强杀招。Hook 脚本可以通过写入由环境变量
CLAUDE_ENV_FILE指定的临时.sh文件,来注入后续所有 Bash 工具可见的环境变量。
边界条件
- 监听列表的排他性:系统只监听
matcher中定义的文件和 Hook 动态返回的watchPaths。它不会监听全量源代码。 - Shell 命令驱动:
CwdChanged仅能捕获由 Claude 内部 Bash 工具执行导致的目录变化。如果你在外部终端cd,Claude 本进程无法直接感知。 - 环境覆盖顺序:通过
FileChanged注入的环境变量具有极高的优先级,它们会按固定顺序拼接到后续 Bash 进程的启动脚本中。 - Watcher 重启开销:每次
CwdChanged触发都可能导致监听器的重启。因此,应避免在watchPaths中添加过于庞大的目录树。
延伸阅读
- 如果你想了解这些环境脚本最后如何进入 Bash 执行链,请看
sessionEnvironment.ts。 - 如果你关项目级的指令注入,请看 InstructionsLoaded:角色与约束的动态加载。
- 如果你对 Hook 的配置方式感兴趣,请看 Hook 系统架构解析。
源码锚点
claude-code-opensource/src/utils/hooks/fileChangedWatcher.ts—CwdChanged/FileChanged的核心运行时逻辑。
📄 src/utils/hooks/fileChangedWatcher.ts — `CwdChanged` / `FileChanged` 的核心运行时逻辑。
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 变化的来源捕获。
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.ts—CLAUDE_ENV_FILE环境脚本的清理与拼接实现。
📄 src/utils/sessionEnvironment.ts — `CLAUDE_ENV_FILE` 环境脚本的清理与拼接实现。
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)}`)
}
}
}