CLAUDE.md 与 Rules:分层装配、路径补载与“条件触发”的指令链
核心概念
在 Claude Code 的项目治理体系中,.claude 目录及其内部的 CLAUDE.md 和 rules/ 文件夹并非简单的说明文档存放地。它们共同构成了一套分层、分时机的指令装配系统。
这套系统的本质在于解决“模型上下文污染”与“指令精准度”之间的矛盾。并非所有的规则都应该在会话启动时一古脑塞进模型,那样会迅速耗尽 Context Window 并分散模型的注意力。Claude Code 的做法是建立一套指令装配链:基础指令在启动时静默注入,而特定路径相关的规则(Path-scoped rules)则在模型真正访问相关文件时才动态补载。这种“按需加载”的机制让模型能够实时获取最相关的开发指南、架构说明或编码规范。
源码级拆解
指令的加载逻辑集中在 claude-code-opensource/src/utils/claudemd.ts 中。
四层优先级装配
系统严格定义了指令的来源层级,按照从低优先级到高优先级的顺序排列(模型上下文中的顺序则是低优先级在前,高优先级在后,以确保近处的规则权重更高):
- Managed Memory:系统级管理指令(如
/etc/claude-code/CLAUDE.md)。 - User Memory:用户全局指令(如
~/.claude/CLAUDE.md)。 - Project Memory:项目级指令(
.claude/CLAUDE.md及项目根目录下的CLAUDE.md)。 - Local Memory:本地忽略的指令(
CLAUDE.local.md),通常用于存放不希望提交到 Git 的个人笔记或调试指南。
启动时的预加载(Eager Loading)
当会话启动时,getMemoryFiles() 会从当前工作目录(CWD)向根目录递归回溯。在每一层级中,它会扫描 CLAUDE.md、.claude/CLAUDE.md 以及 .claude/rules/*.md。
这里有一个关键的实现细节:并非 rules/ 下的所有文件都会在此时加载。系统通过解析 Markdown 的 Frontmatter 来进行分流:
- 无条件规则(Unconditional Rules):没有定义
paths:属性的规则,会被视为基础指令在启动时直接注入。 - 条件规则(Conditional Rules):带有
paths:属性(支持 Glob 语法)的规则,会被暂时忽略,留待后续触发。
路径关联的懒加载(Lazy Loading)
真正体现“智能”的是路径补载机制。在 claude-code-opensource/src/utils/attachments.ts 中,当模型调用工具读取某个具体文件时,系统会触发 processConditionedMdRules()。
它会根据目标文件的路径,在之前扫描到的规则池中寻找匹配的条件规则。如果路径命中,这些规则会作为“嵌套记忆(Nested Memory)”被实时注入上下文。值得注意的是,匹配基准是不同的:项目级规则相对于项目根目录匹配,而用户级/系统级规则则相对于当前 CWD 匹配。这种区分确保了全局规则在不同项目间的一致性。
复杂场景下的保护逻辑
- Git Worktree 去重:在 Worktree 环境下,系统会主动检测主仓库与 Worktree 目录的重合,防止同一份受版本控制的
CLAUDE.md被重复注入,从而节省 Context。 - 递归搜索与符号链接:
processMdRules()支持递归扫描子目录,这意味着你可以将规则按模块组织在.claude/rules/frontend/或.claude/rules/backend/中,系统会自动平铺并索引。 - 缓存与状态隔离:加载后的指令会预填到
readFileState缓存中。如果指令经过了 Frontmatter 剥离或 HTML 注释清理,系统会将其标记为isPartialView,以确保当模型试图编辑这些指令文件时,必须先读取磁盘上的原始内容,防止破坏文件结构。
实战注意事项
第一,指令不只是 CLAUDE.md。即使你删除了项目里的文件,Claude 仍可能受到 ~/.claude/CLAUDE.md 中定义的个人习惯影响。
第二,rules/ 的触发是“精准打击”。如果你在规则中定义了 paths: ["src/ui/**"],那么当模型只在 tests/ 目录下活动时,这条规则永远不会进入上下文。
第三,路径匹配的严格性。Glob 匹配不能逃出其定义的基准目录。如果你尝试在用户全局规则中用相对路径去匹配某个特定项目的深层文件,可能会因为路径计算越界而导致匹配失败。
第四,环境变量的闸门。目前通过 --add-dir 添加的额外目录是否包含指令发现,受 CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD 环境变量的显式控制。
第五,与 Auto Memory 的区别。这套机制是静态的、显式定义的,而 Auto Memory 是由模型根据对话历史自动提取并存储在 history.jsonl 或 MEMORY.md 中的,两者的处理流程和压缩算法完全不同。
延伸阅读
如果你关心这些指令在进入上下文时是否会触发用户的安全审计,可以查看 Instructions Loaded Hook 的实现。
如果你想了解模型如何处理超出窗口限制的长指令,建议阅读 Context Window & Truncation 机制。
如果你打算开发自动化规则生成工具,应重点分析 claudemd.ts 中对 Frontmatter 的解析算法。
源码锚点
claude-code-opensource/src/utils/claudemd.ts: 指令分层、目录回溯扫描及 Frontmatter 解析的核心实现。
📄 src/utils/claudemd.ts — 指令分层、目录回溯扫描及 Frontmatter 解析的核心实现。
parseFrontmatter,
splitPathInFrontmatter,
} from './frontmatterParser.js'
import { getFsImplementation, safeResolvePath } from './fsOperations.js'claude-code-opensource/src/utils/attachments.ts: 条件规则的路径匹配算法与动态注入逻辑。
📄 src/utils/attachments.ts — 条件规则的路径匹配算法与动态注入逻辑。
type Tools,
type ToolUseContext,
type ToolPermissionContext,
} from '../Tool.js'
import {claude-code-opensource/src/screens/REPL.tsx: 启动时指令预加载与readFileState初始化的接线点。
📄 src/screens/REPL.tsx — 启动时指令预加载与 `readFileState` 初始化的接线点。
const readFileState = useRef(initialReadFileState);
const bashTools = useRef(new Set<string>());
const bashToolsProcessedIdx = useRef(0);
// Session-scoped skill discovery tracking (feeds was_discovered on
// tengu_skill_tool_invocation). Must persist across getToolUseContext
// rebuilds within a session: turn-0 discovery writes via processUserInput
// before onQuery builds its own context, and discovery on turn N must
// still attribute a SkillTool call on turn N+k. Cleared in clearConversation.
const discoveredSkillNamesRef = useRef(new Set<string>());
// Session-level dedup for nested_memory CLAUDE.md attachments.
// readFileState is a 100-entry LRU; once it evicts a CLAUDE.md path,
// the next discovery cycle re-injects it. Cleared in clearConversation.
const loadedNestedMemoryPathsRef = useRef(new Set<string>());
// Helper to restore read file state from messages (used for resume flows)
// This allows Claude to edit files that were read in previous sessions
const restoreReadFileState = useCallback((messages: MessageType[], cwd: string) => {
const extracted = extractReadFilesFromMessages(messages, cwd, READ_FILE_STATE_CACHE_SIZE);
readFileState.current = mergeFileStateCaches(readFileState.current, extracted);
for (const tool of extractBashToolsFromMessages(messages)) {
bashTools.current.add(tool);
}
}, []);claude-code-opensource/src/utils/context.ts: 定义了指令在发送给模型时的最终拼装顺序。
📄 src/utils/context.ts — 定义了指令在发送给模型时的最终拼装顺序。
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000
// Maximum output tokens for compact operations
export const COMPACT_MAX_OUTPUT_TOKENS = 20_000
// Default max output tokens
const MAX_OUTPUT_TOKENS_DEFAULT = 32_000
const MAX_OUTPUT_TOKENS_UPPER_LIMIT = 64_000
// Capped default for slot-reservation optimization. BQ p99 output = 4,911
// tokens, so 32k/64k defaults over-reserve 8-16× slot capacity. With the cap
// enabled, <1% of requests hit the limit; those get one clean retry at 64k
// (see query.ts max_output_tokens_escalate). Cap is applied in
// claude.ts:getMaxOutputTokensForModel to avoid the growthbook→betas→context
// import cycle.
export const CAPPED_DEFAULT_MAX_TOKENS = 8_000
export const ESCALATED_MAX_TOKENS = 64_000
/**
* Check if 1M context is disabled via environment variable.
* Used by C4E admins to disable 1M context for HIPAA compliance.
*/
export function is1mContextDisabled(): boolean {
return isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT)
}