Claude Code 源码揭秘:CLAUDE.md,一个 Markdown 文件如何驯服 AI
💡 阅读前记得关注+星标,及时获取更新推送
「Claude Code 源码揭秘」系列的第十四篇,上一篇《Claude Code 源码揭秘:MCP 协议,让 AI 长出无限多的手》,说的是怎么通过标准化接口「接」外部工具进来。但有了工具,还得有规则——哪些工具能用,怎么用,有什么禁忌。

你可能在项目根目录见过一个叫 CLAUDE.md 的文件。
这不是普通的文档,而是一份「AI 行为规范」——告诉 Claude Code 在这个项目里该怎么干活、不该干什么、遵循什么规则。
翻源码才发现,这套规则系统比想象中精细。不只是「把文件内容塞进 prompt」那么简单,而是有完整的搜索策略、解析逻辑、优先级机制、权限控制。
我之前做过企业级的配置管理系统,也有类似的「规则覆盖」需求——全局配置、租户配置、项目配置,一层层叠加。Claude Code 的 CLAUDE.md 系统,思路是一样的,但针对 AI 场景做了很多特殊设计。
文件搜索:从脚下往上找
Claude Code 找 CLAUDE.md 的策略是「向上搜索」:
const CLAUDE_MD_FILES = [ 'CLAUDE.md', // 首选 '.claude.md', // 隐藏版本 'claude.md', // 小写版本 '.claude/CLAUDE.md', // .claude 子目录 '.claude/instructions.md', // 说明文件];
从当前目录开始,依次检查这些文件名。没找到就往上一级目录找,一直找到根目录。最后还会检查用户主目录 ~/.claude/CLAUDE.md。
function findClaudeMd(startDir?: string): string | null { let dir = startDir || process.cwd(); // 向上遍历 while (dir !== path.dirname(dir)) { for (const filename of CLAUDE_MD_FILES) { const filePath = path.join(dir, filename); if (fs.existsSync(filePath)) { return filePath; // 找到就返回 } } dir = path.dirname(dir); } // 最后检查家目录 const homeClaudeMd = path.join(os.homedir(), '.claude', 'CLAUDE.md'); if (fs.existsSync(homeClaudeMd)) { return homeClaudeMd; } return null;}
为什么要向上搜索?因为你可能在子目录里工作。比如在 src/components/ 目录下,但 CLAUDE.md 在项目根目录。向上搜索能确保找到它。
这个设计让我想起 Git 找 .git 目录的策略——也是从当前目录往上找。很多工具都用这种模式,成熟可靠。
解析过程
CLAUDE.md 是 Markdown 文件,解析时会按标题分段:
function parseClaudeMd(filePath: string): ClaudeMdSection[] { const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); const sections: ClaudeMdSection[] = []; let currentSection = { title: '', level: 0, content: '' }; for (const line of lines) { // 匹配 Markdown 标题 const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { // 保存上一个 section if (currentSection.title || currentSection.content.trim()) { sections.push(currentSection); } // 开始新 section currentSection = { title: headingMatch[2], level: headingMatch[1].length, content: '' }; } else { currentSection.content += line + '\n'; } } return sections;}
然后从各个 section 提取规则:
function extractRules(sections: ClaudeMdSection[]): ProjectRules { const rules: ProjectRules = {}; for (const section of sections) { const titleLower = section.title.toLowerCase(); if (titleLower.includes('instruction')) { // 指令:累积 rules.instructions = (rules.instructions || '') + section.content; } else if (titleLower.includes('allowed tool')) { // 允许的工具:解析列表 rules.allowedTools = parseListFromContent(section.content); } else if (titleLower.includes('disallowed tool')) { // 禁止的工具:解析列表 rules.disallowedTools = parseListFromContent(section.content); } else if (titleLower.includes('permission')) { // 权限模式 rules.permissionMode = section.content.trim().split('\n')[0]; } else if (titleLower.includes('model')) { // 模型选择 rules.model = section.content.trim().split('\n')[0]; } // ... 其他规则类型 } return rules;}
标题里包含 instruction 就是指令,包含 allowed tool 就是工具白名单… 这种「约定优于配置」的设计,让 CLAUDE.md 的格式很灵活。
支持的规则类型
根据源码,CLAUDE.md 支持这些规则:
# Instructions这里写给 AI 的指令,会累积到系统提示里。# Allowed Tools- Read- Glob- Grep# Disallowed Tools- Bash- Write# Permission ModeacceptEdits# Modelsonnet# System Prompt Addition额外的系统提示内容...# Custom Rules- **Rule1**: 描述...- **Rule2**: 描述...# Memory- **ProjectType**: TypeScript- **Framework**: React
最常用的是 Instructions 和 Allowed/Disallowed Tools。前者告诉 AI 该怎么做,后者限制它能用哪些工具。
权限规则语法
工具权限不只是「允许/禁止」这么简单,还支持细粒度控制:
// 规则格式"Bash" // 所有 Bash 命令"Bash(npm:*)" // npm 开头的命令"Bash(npm install:*)" // npm install 命令"Read(/home/**)" // /home 及子目录的读取"Write(src/*.ts)" // src 目录下的 TypeScript 文件写入"Edit(*.md)" // 所有 Markdown 文件编辑
正则解析:
const RULE_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)(?:\(([^)]*)\))?$/;// 匹配:工具名(参数)
比如你想允许 AI 读取代码但不能读取配置文件:
# Allowed Tools- Read(src/**)- Read(tests/**)# Disallowed Tools- Read(*.env)- Read(**/secrets/**)
这种精细控制在企业环境里特别有用。我之前做安全审计的时候,经常遇到「允许访问某些目录,禁止访问某些目录」的需求。CLAUDE.md 的这套语法刚好能满足。
规则优先级计算
当多个规则冲突时,怎么决定哪个生效?
function calculatePriority(type, source, hasParams) { let priority = 0; // deny 优先于 allow (+1000) if (type === 'deny') { priority += 1000; } // 具体规则优先于通用规则 (+100) if (hasParams) { priority += 100; } // 来源优先级 const sourcePriorities = { cli: 50, // 命令行参数 policy: 40, // 策略文件 project: 30, // 项目设置 settings: 20, // 用户设置 session: 10, // 会话级 runtime: 0, // 运行时 }; priority += sourcePriorities[source]; return priority;}
三个原则:
-
• deny 优先于 allow:安全第一,禁止的优先级更高 -
• 具体优先于通用: Read(src/*.ts)比Read优先级高 -
• 近的优先于远的:命令行 > 项目 > 全局
实际检查时:
check(input, defaultAllow = true) { // 首先检查 deny 规则 for (const rule of this.denyRules) { if (matches(rule, input)) { return { allowed: false, reason: `Denied by: ${rule.raw}` }; } } // 然后检查 allow 规则 for (const rule of this.allowRules) { if (matches(rule, input)) { return { allowed: true, reason: `Allowed by: ${rule.raw}` }; } } // 无匹配,使用默认值 return { allowed: defaultAllow };}
deny 规则先检查,一旦命中直接拒绝,不管有没有 allow 规则。这是安全系统的经典模式——「默认拒绝」比「默认允许」更安全。
多文件合并
如果同时存在全局和项目的 CLAUDE.md,怎么合并?
function mergeRules(base, override) { return { // 覆盖型字段:后者覆盖前者 instructions: override.instructions || base.instructions, allowedTools: override.allowedTools || base.allowedTools, permissionMode: override.permissionMode || base.permissionMode, model: override.model || base.model, // 累积型字段:合并而非覆盖 customRules: [ ...(base.customRules || []), ...(override.customRules || []), ], memory: { ...base.memory, ...override.memory }, };}
大部分字段是「覆盖」——项目配置完全替代全局配置。但 customRules 和 memory 是「累积」——两边的都保留。
这个设计很合理。指令、工具列表这些,项目有自己的就用项目的。但自定义规则和记忆,可能全局和项目都有有用的内容,应该都保留。
注入到 System Prompt
最终,CLAUDE.md 的内容会被注入到系统提示里:
injectIntoSystemPrompt(basePrompt) { const info = this.parse(); if (!info.exists || !info.content.trim()) { return basePrompt; } return `${basePrompt}# claudeMdCodebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.Contents of ${this.claudeMdPath}:${info.content} IMPORTANT: this context may or may not be relevant to your tasks.`;}
注意那句「OVERRIDE any default behavior」——明确告诉模型,CLAUDE.md 里的规则要覆盖默认行为。
作为附件时,优先级是 10(1 是最高,数字越小越优先):
{ type: 'claudeMd', content: `<system-reminder>...\n${content}\n...</system-reminder>`, label: 'CLAUDE.md', priority: 10,}
比环境信息(priority: 40)优先,但比关键系统提醒(priority: 1)低。
文件变化监听
CLAUDE.md 改了会怎样?Claude Code 有监听机制:
watch(callback) { const watcher = fs.watch(this.claudeMdPath, (eventType) => { if (eventType === 'change') { // 清除缓存,重新解析 this.cachedInfo = null; callback(this.parse()); } }); return watcher;}
改了文件,下次请求就会用新的规则。不需要重启 Claude Code。
完整的配置优先级链
把所有配置来源串起来,每一层都可以覆盖上一层:

翻完这部分代码,我最大的感受是:CLAUDE.md 不只是一个文档,而是一套配置语言。
它用 Markdown 的语法,实现了规则定义、权限控制、行为定制。对普通用户来说,写个 Markdown 文件就能控制 AI 的行为,门槛很低。对高级用户来说,细粒度的权限语法能满足复杂需求。
这套设计的精髓是springboot用的「约定优于配置」——不需要学新的配置语法,用 Markdown 的标题和列表就能表达规则。
源码分析完了,但光知道原理不够。接下来聊聊怎么真正用好 CLAUDE.md——这部分内容来自我这大半年的实战经验,加上从 Reddit、Hacker News、Twitter 社区里淘来的高手心得。
写好 CLAUDE.md 的第一原则:少即是多
很多人第一反应是把 CLAUDE.md 塞得满满当当——代码规范、技术文档、架构说明、命名约定… 恨不得把整个团队 wiki 搬进去。
这是大忌。
HumanLayer 团队做过一个研究,结论挺扎心的:前沿思考模型大约能可靠地遵循 150-200 条指令,再多就开始丢指令了,而且不是”忘掉最后加的”——是所有指令的遵循质量均匀下降。换句话说,你写了 300 条规则,等于一条都没写好。
更要命的是,Claude Code 的系统提示本身就包含约 50 条内置指令。留给你 CLAUDE.md 的”指令预算”,可能也就 100-150 条。
Anthropic 官方文档给的建议也很直白:对文件里的每一行,问自己”如果去掉这行,Claude 会犯错吗?”如果不会,就删掉。
我自己的经验:CLAUDE.md 膨胀到一定程度后,Claude 开始无视你的规则。你明明写了”不要用 npm,用 pnpm”,它还是给你跑 npm install。不是它故意的,是你的规则淹没在一堆噪音里了。
Shrivu Shankar(Claude Code 的重度用户,帖子在 Hacker News 拿了 500+ 赞)分享过他的做法: 团队的 monorepo CLAUDE.md 严格维护,保持在 13KB 左右。只记录 30% 以上工程师会用到的工具和 API,其余的放在各个产品或库的独立 markdown 文件里。他们甚至开始给 CLAUDE.md 分配”token 预算”,像管理实际资源一样管理这个文件。
用指针代替复制
另一个常见的坑:把代码片段贴到 CLAUDE.md 里当示例。
问题在于代码会变,但 CLAUDE.md 里的片段不会自动更新。过一段时间,你的”示例”和实际代码就对不上了,Claude 反而会被误导。
更好的做法是用 @ 语法引用实际文件:
# 项目概述See @README.md for project overview and @package.json for available npm commands.# Git 工作流@docs/git-instructions.md# 个人偏好@~/.claude/my-project-instructions.md
这样 Claude 每次都去读源文件,信息永远是最新的。
别让 LLM 干 linter 的活
社区里被吐槽最多的一个反模式——把代码风格规范塞进 CLAUDE.md。
“缩进用 2 空格””字符串用单引号””组件名用 PascalCase”… 这类东西适合配置 ESLint、Prettier、Biome 这些工具,而不是写成自然语言让 AI 去”遵守”。
原因很简单:linter 是确定性的,每次都给你相同的结果,速度快,成本低。LLM 是概率性的,执行慢,消耗 token,还可能忘掉你的规则。
正确的姿势是用 Hooks 在文件保存后自动跑 formatter:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH" } ] } ] }}
确定的事情交给确定性工具,模糊的事情才需要 AI 判断。
CLAUDE.md 应该写什么
翻了大量社区案例和 Anthropic 官方建议,我总结了一个”三段论”——WHAT、WHY、HOW:
WHAT——这是什么项目,技术栈地图
告诉 Claude 项目的全貌:用了什么框架、什么语言、核心模块在哪。特别是 monorepo,一定要交代清楚各个 app 和 package 的职责,否则 Claude 会在代码库里乱窜。
# 项目概述FastAPI REST API,用户认证和画像管理。SQLAlchemy 做数据库操作,Pydantic 做数据校验。# 关键目录- app/models/ — 数据库模型- app/api/ — 路由处理- app/core/ — 配置和工具类
WHY——为什么这么做,设计决策
这是最多人忽略的,也是最有价值的部分。Claude 只能看到代码长什么样,但不知道为什么这么写。
# 设计决策- 支付回调走 MQ 而不是直接处理:因为第三方回调接口不稳定, 需要削峰和重试- 用 Redis 分布式锁而不是数据库锁:Redisson 的看门狗机制 能自动续期,避免锁超时- 订单状态不用枚举:业务状态有 20 多种,用状态模式 + 配置表
有了这些,Claude 给建议的时候就不会”脱离项目实际”——它知道你为什么选了这条路,不会傻乎乎地建议你走另一条。
HOW——怎么干活,命令和流程
构建命令、测试命令、部署流程。但注意,不要面面俱到,只写最常用的:
# 常用命令- bun run dev: 启动开发服务器- bun run test: 跑所有测试- bun run test:unit -- path/to/file: 跑单个测试文件(优先用这个)- bun run typecheck: 类型检查# 工作流- 改完代码后先跑 typecheck- 单测优先跑单个文件,别一上来就跑全量- commit message 用中文,格式:类型: 描述
从 Git 历史里挖金子
CLAUDE.md 是静态的”底座”,但项目是活的。让 Claude 理解项目的演进脉络,Git 历史是最好的材料。
提交记录是个宝藏
我现在的习惯是,开工前先让 Claude 看最近的 Git 提交记录:
# 最近 20 条提交,带上改动的文件列表git log -20 --oneline --name-status# 某个功能分支的完整变更git log main..feature-branch --stat# 看某个文件的演进历史git log -p -- path/to/important/file.ts
Claude 拿到这些,能快速了解:最近在搞什么功能、哪些文件是热区、提交信息里的业务术语和技术决策。
有次我让 Claude 帮忙重构支付模块,先喂了支付相关文件的 git blame 和最近的提交记录。它直接就能理解为啥某些”奇怪”的代码逻辑是那样写的——因为提交信息里写着”临时规避 XX 支付接口的并发问题”。
Reddit 的 r/ClaudeAI 上有个哥们分享过,他维护一个十万行的老项目,每次让 Claude 干活前,会先生成一份”最近一周高频变更文件的 diff 摘要”。Claude 拿到这个,基本就知道哪块是活跃区域,哪块是稳定代码了。
Diff 比全文更有信息量
别总是把整个文件扔给 Claude,很多时候一个精准的 diff 更有价值:
# 看某次提交改了啥git show commit-hash# 对比两个分支的差异git diff main...feature-branch# 只看某个目录的变更git diff HEAD~5 HEAD -- src/payment/
用 SessionStart Hook 自动注入 Git 状态
每次新开会话都要手动喂 Git 信息,太蠢了。聪明的做法是用 Hook 自动注入:
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "git status --short && echo '---' && git log --oneline -5" } ] } ] }}
这样每次启动 Claude Code,它自动知道当前 Git 状态和最近几次提交。stdout 的内容会直接进入 Claude 的上下文。
Shrivu Shankar 更进一步,他有个 /catchup 自定义命令——让 Claude 读取当前分支所有未提交的变更。当上下文窗口满了,/clear 清掉历史,再 /catchup 重新加载工作进展,无缝衔接。
跨会话不丢活——HANDOFF.md 模式
Claude Code 的一个痛点是上下文有限。长任务做到一半,context 满了,compact 之后细节丢了一大半。
ykdojo 在他的 45 Tips 仓库里提出了 HANDOFF.md 模式——在切换会话前,让 Claude 把当前进展写成一份交接文档:
“把计划的剩余部分写到 HANDOFF.md。解释你已经尝试了什么,什么有效、什么无效,让下一个 agent 只加载这个文件就能接手任务。”
下一个会话只需要读这个文件,就能从断点继续。不用从头解释需求,不用重新建立上下文。
他甚至做了一个 /handoff 自定义命令来自动化这个过程——检查是否已有 HANDOFF.md,有就读取并更新,没有就创建。包含目标、进展、哪些方法可行、哪些不行、下一步计划。
类似的思路,Alabê Duarte 的做法是让 Claude 把对项目的理解写成 markdown 文件(比如 SEARCH_FEATURE.md),然后 /clear 清空上下文,再用 @SEARCH_FEATURE.md 引用这个文件开始新阶段。每个阶段完成就 git commit,保证可回溯。
让 Claude 团队协作——把 CLAUDE.md 当代码管理
CLAUDE.md 不是某个人的私有物,应该提交到 Git 里,让整个团队受益。
Anthropic 官方建议:用 # 键随时给 Claude 添加指令(比如你发现它老犯某个错,按 # 加一条规则),然后把 CLAUDE.md 的变更一起 commit。团队成员都能看到、都能补充。
Claude Code 团队自己的做法更极端——他们定期审查 CI/CD 中 Claude Code GitHub Action 的日志,找出 Claude 反复犯的错误,然后更新 CLAUDE.md。形成一个数据驱动的飞轮:
Bug → 改进 CLAUDE.md / CLI 工具 → 更好的 Agent 行为
还有个哥们的做法让我印象深刻:他让 Claude 自己审查自己。用 claude --resume 恢复几天前的会话,问 Agent “你当时是怎么解决那个错误的?”,然后把答案提炼成新的规则加进 CLAUDE.md。
CLAUDE.md vs Skills vs Hooks——该用哪个
到了现在这个版本,Claude Code 有三套”规则注入”机制,很多人搞混了。简单说——
CLAUDE.md 是”永远在线”的底座。每次会话都加载,放那些项目级别的通用规则。但正因为永远在线,所以要克制,别塞太多东西。
Skills 是”按需加载”的专家。放在 .claude/skills/ 目录下,Claude 会根据任务自动判断要不要读取。适合放那些特定场景的详细知识,比如”GraphQL schema 设计规范””测试模式指南”。好处是不会每次都占用 context,只有相关的时候才加载。
Hooks 是”确定性执行”的铁规则。CLAUDE.md 里的规则是”建议性”的,Claude 可能忘掉。Hooks 是确定性的,触发了就一定执行。适合放那些不允许有例外的操作——跑 formatter、跑测试、阻止往敏感目录写文件。
一句话总结:CLAUDE.md 管认知,Skills 管知识,Hooks 管纪律。
我的 CLAUDE.md 模板
聊了这么多,给一个我实际在用的模板,大家可以在这个基础上改:
# CLAUDE.md## 项目概述[一两句话说清楚这是什么项目]## 技术栈- 语言/框架:[xxx]- 数据库:[xxx]- 包管理器:[bun/pnpm/npm]## 常用命令- 开发:[命令]- 测试(单文件):[命令]- 类型检查:[命令]- 构建:[命令]## 工作流- 改完代码先跑 typecheck- 提交前跑一遍相关测试- commit message 格式:类型: 描述- 分支命名:feature/xxx, fix/xxx## 设计决策- [为什么选了 A 而不是 B]- [某个"奇怪"的设计背后的原因]## 陷阱和注意事项- [哪个方法不是幂等的]- [哪个表有并发更新问题]- [哪些文件不能碰]## 补充上下文See @docs/architecture.md for system designSee @README.md for setup instructions
模板里最有价值的是”设计决策”和”陷阱”这两块,别省略——这是 Claude 光看代码看不出来的东西。
写到这里,把源码分析和使用技巧串起来看,CLAUDE.md 这个东西的设计哲学其实很清晰:用最低的门槛(写 Markdown)获得最大的控制力(定制 AI 行为)。
但门槛低不等于能随便写。社区里那些用得好的人,无一例外都在”精炼”上下了大功夫——像写代码一样写 CLAUDE.md,定期 review、定期裁剪、根据 Claude 的实际行为反复调优。
说到底,CLAUDE.md 不是写完就扔那儿的文档,而是一份活着的、不断演化的 AI 行为契约。你投入多少心思去维护它,它就回报你多少生产力。
下一篇聊多云适配。Claude Code 是怎么支持 AWS Bedrock、Google Vertex 这些云平台的?API 差异怎么抹平?
本文基于 Claude Code 2.0.76 版本源码分析,主要文件:src/rules/index.ts、src/config/claude-md-parser.ts、src/permissions/rule-parser.ts。社区实践部分参考了 Anthropic 官方文档、Shrivu Shankar、ykdojo、HumanLayer、Alabê Duarte 等社区贡献者的分享。
夜雨聆风
