Claude Code官方文档:https://code.claude.com/docs/zh-CN/overview

Claude Code泄露源码:https://github.com/NanmiCoder/cc-haha

一、 CLAUDE.md

Claude Code 的上下文工程是一个分层体系:从静态的 CLAUDE.md,到动态的 MEMORY.md,到按需的 SKILL.md 和 Rules,再到执行层的 Hooks——它们绝大多数最终都作为 user-role context message 注入到对话历史,而不是 system prompt。理解这个分层,才能真正做好上下文工程。

(一) CLAUDE.md简介

CLAUDE.md 最常见的的位置,会放到这两个地方,一个是项目目录下面的 CLAUDE.md,一个是项目目录下面 .claude 文件夹下的 CLAUDE.md,总共包含11种放置位置。内容如下:

  • 项目介绍,如:目录介绍,模块介绍,命令介绍
  • 开发规范,如:命名规范,代码风格,Git提交规范
  • 注意事项,如:特别约束,采坑文档

(二) CLAUDE.md的11种位置

1. 组织级3种

  • /组织级配置目录/CLAUDE.md
  • /组织级配置目录/.claude/CLAUDE.md
  • /组织级配置目录/.claude/rules/*.md

公司的全局编码标准、安全策略、合规要求等信息,适合写到这里

2. 用户级2种

  • C:\Users\admin/.claude/CLAUDE.md
  • C:\Users\admin/.claude/rules/*.md

个人的代码风格偏好、个人工具快捷方式等信息,适合写到这里

3. 项目级5种(最常用)

  • /项目目录/CLAUDE.md
  • /项目目录/.claude/CLAUDE.md
  • /项目目录/.claude/rules/*.md
  • /项目目录/module/rules/*.md
  • /项目目录/module/sub_module/CLAUDE.md

单个项目的项目架构、编码标准、常用工作流等信息,适合写到这里

4. 本地级1种

  • /项目目录/module/sub_module/CLAUDE.local.md

个人的沙盒 URL、偏好的测试数据等信息,适合写到这里

5. 参考图

(三) CLAUDE.md的执行过程

1. 拼接用户参数的函数:prependUserContext

从claude code泄露源码中发现的关键函数:prependUserContext

很多人会下意识以为 CLAUDE.md 的内容是被 Claude Code 拼到 system prompt 里发给模型的。这个理解其实不准确,下面我们看一下源码里它实际是怎么被传过去的

关键源码在 src/utils/api.ts:449,函数名叫prependUserContext

// 定义一个函数 prependUserContext,作用是把额外上下文"塞"到对话历史最前面
export function prependUserContext(
  messages: Message[],              // 参数 1:原本的对话历史(一个消息数组)
  context: { [k: string]: string }, // 参数 2:要注入的上下文对象,CLAUDE.md 内容就装在这里
): Message[] {
// 如果是测试环境,原样返回不做处理,避免污染测试
if (process.env.NODE_ENV === 'test') return messages
// 如果 context 对象是空的,没东西要塞,直接返回
if (Object.entries(context).length === 0) return messages

// 返回一个全新的数组:新造的消息在前,原对话历史在后
return [
    // 临时造一条"用户消息"插到对话最前面
    createUserMessage({
      // 消息内容是下面这一大段文本,被 <system-reminder> 标签包起来
      content: `<system-reminder>
As you answer the user's questions, you can use the following context:
${Object.entries(context).map(([k, v]) => `# ${k}\n${v}`).join('\n')}

IMPORTANT: this context may or may not be relevant to your tasks.
You should not respond to this context unless it is highly relevant to your task.
</system-reminder>`,
      isMeta: true,                 // 打个"元消息"标记:这是系统造的,不是真用户输入
    }),
    ...messages,                    // 把原本的对话历史展开拼在后面
  ]
}

2. 分析执行方法

1. 接收一个 messages 数组,再接一个 context 对象(CLAUDE.md的内容就放在 context 里面)
2. 把 context 拼成一段文本,外面套一层<system-reminder>标签
3. 用 createUserMessage() 创建一条 user message,再用 ...messages 拼到原 messages 数组的前面
所以CLAUDE.md实际上是以一条 user message 的形式被塞到对话历史最前面的,它不是 system prompt 的一部分

3. 分析执行方法得到的执行过程

读取 CLAUDE.md
    ↓
形成 context 对象
    ↓
prependUserContext(messages, context)
    ↓
创建一条 user-role meta message
    ↓
插入 messages 最前面
    ↓
连同 system prompt、tools、历史消息一起发给 Claude API

4. 总结思考

_**CLAUDE.md**_** = Claude Code 应用层的项目上下文;最终落到 API 请求时 = 一条插在最前面的
user-role meta message。**

思考一:CLAUDE.md的约束力其实没有想象中那么强。它在模型那里只是一段"user message",并不是系统层指令

思考二:注入文本末尾那句

“IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task. ”

翻译为中文:“重要提示:此上下文可能与您的任务相关,也可能不相关。除非与您的任务高度相关,否则您不应响应此上下文。”

是源码里写死的,相当于明确告诉模型可以判断当前任务和这段上下文是否相关。所以有时候模型会觉得任务和CLAUDE.md关系不大,就忽略掉部分规则,这并不是 AI 不听话,是源码层面就允许它这样做

思考三:如果你希望让 AI 强制遵守某些规则,CLAUDE.md不是最强的通道。可以考虑用 agent 的 system prompt、或者 hooks 这种程序级约束来实现

(四) CLAUDE.md的硬性限制清单

以下是通过claude code源码发掘到的CLAUDE.md的限制

二、 MEMORY.md

_MEMORY.md__ 是 Claude Code __Auto Memory(自动记忆)机制的入口文件,用来让 Claude 在不同会话之间保留它“自己学到的项目知识”。它属于典型的上下文工程(Context Engineering)_组件,但不是 System Prompt,而是被当作上下文注入给 Claude 使用。

官方文档:https://code.claude.com/docs/zh-CN/memory

我在一次让AI编写接口用例时,它没有遵守格式规范,漏掉了一部分要求必须写的注释内容。我就反问AI,让AI反思并总结这次犯错的原因,以及以后如何避免。最后AI生成了如下的记忆文件,来避免下次犯同样的错:~.claude\projects\D–workSpace-001-test-automation\memory\MEMORY.md。

(一) MEMORY.md 是什么?

Claude Code 每个会话都会从新的上下文窗口开始,因此它本来不会记得上次调试过什么、项目有什么特殊命令、用户偏好是什么。Auto Memory 用来解决这个问题:Claude 会在工作过程中,把它认为未来有用的信息写入记忆文件,例如构建命令、调试经验、架构笔记、代码风格偏好和工作流习惯。

它不是你手写的项目规范,而是 Claude 根据会话中的修正、偏好和发现自动沉淀出来的内容。

(二) 它解决的痛点

痛点MEMORY.md 如何解决
每次新会话都从零开始会话开始时自动加载记忆,让 Claude 继承项目经验
重复解释项目规则和偏好Claude 可记住“使用 pnpm 而不是 npm”等偏好
调试经验丢失把曾经踩过的坑、特殊命令、环境问题保存下来
长期项目上下文断裂让 Claude 在多次会话中逐渐积累项目知识
人工维护文档成本高Claude 自动判断哪些信息值得记住,无需每次手动写文档

(三) 存放路径

官方当前路径是:

~/.claude/projects/<project>/memory/
├── MEMORY.md
├── debugging.md
├── api-conventions.md
└── ...

<project> 通常根据 Git 仓库派生。同一个 Git 仓库下的所有 worktree 和子目录会共享同一个自动记忆目录;如果不在 Git 仓库中,则使用项目根目录作为依据。

(四) 加载规则

MEMORY.md 并不是无限制全部塞进上下文。

官方规则是:

每次会话启动时加载 MEMORY.md 的前 200 行,或前 25KB,取先到者。

超过部分不会在启动时加载。Claude 会尽量保持 MEMORY.md 简洁,并把详细内容移动到其他主题文件,例如 debugging.mdpatterns.md,这些文件不会启动时加载,而是在需要时由 Claude 通过文件工具按需读取。

这是一种典型的 Progressive Disclosure(渐进式披露) 思路:

启动时加载小索引
需要时再读取详细文件
避免上下文窗口被无关信息撑爆

(五) 如何使用

1. 查看和编辑

在 Claude Code 中使用:

/memory

/memory 可以查看当前会话加载的 CLAUDE.mdCLAUDE.local.md、rules 文件,并提供打开自动记忆文件夹的入口,还可以切换 Auto Memory 开关。

2. 让 Claude 记住某件事

你可以直接说:

记住:这个项目使用 pnpm,不要用 npm。

或者:

Remember that API tests require a local Redis instance.

Claude 会把这些信息保存到 Auto Memory 中。

3. 禁用 Auto Memory

Auto Memory 默认开启。可以通过 /memory 面板关闭,也可以在项目设置(settings.json)中写:

{
  "autoMemoryEnabled": false
}

或者通过环境变量关闭:

$env:CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1"

官方也说明可以使用 autoMemoryEnabled: falseCLAUDE_CODE_DISABLE_AUTO_MEMORY=1 关闭自动记忆。

(六) 在上下文工程中的作用

MEMORY.md 属于上下文工程中的 Context Persistence(上下文持久化)

它的作用不是规定 Claude 必须做什么,而是让 Claude 在每次会话开始时获得一份“历史经验摘要”。

可以这样理解:

CLAUDE.md 负责告诉 Claude:这个项目的规则是什么。
MEMORY.md 负责提醒 Claude:你之前在这个项目里学到了什么。
SKILL.md 负责教 Claude:遇到某类任务时应该怎么做。

官方明确说明,CLAUDE.md 和 Auto Memory 都会在每次对话开始时加载,并且 Claude 会把它们当作上下文,而不是强制配置。

另外,官方还说明 CLAUDE.md 内容是作为 system prompt 之后的 user message 传递的,不是 system prompt 本身;它会影响行为,但不能保证严格遵守。

(七) MEMORY.mdCLAUDE.mdSKILL.md 区别

对比项MEMORY.mdCLAUDE.mdSKILL.md
核心定位Claude 的自动记忆人类写给 Claude 的项目说明和规则可复用任务技能
谁写Claude 自动写,也可用户要求记住开发者、团队、组织开发者、团队、插件作者
内容类型学到的经验、偏好、调试发现项目规则、架构、命令、约定某类任务的步骤、流程、工具说明
加载时机每次会话启动加载前 200 行或 25KB每次会话启动加载,通常完整加载相关任务触发时按需加载
适合放什么“上次发现测试需要 Redis”“项目使用 pnpm,API 在 src/api”“如何生成 PR 摘要”“如何做代码审查”
是否版本控制默认机器本地,不跨机器共享项目级通常提交 Git项目级或插件级可提交 Git
是否硬约束
是否占用上下文是,但有 200 行/25KB 限制是,文件越大越占 token触发时才占用主要上下文
主要风险记住错误信息、过期信息、个人偏好污染文件过长、规则冲突、过度泛化技能描述不准导致误触发或不触发

(八) 什么时候用哪个?

1. 用 CLAUDE.md

适合写稳定、团队共享、每次都需要知道的信息:

# Build
- 使用 pnpm install
- 使用 pnpm test 运行测试


# Architecture
- API 位于 src/api/
- 数据库使用 PostgreSQL
- ORM 使用 Prisma

官方建议 CLAUDE.md 用于 coding standards、workflows、project architecture 等内容。

2. 用 MEMORY.md

适合保存 Claude 在工作中发现的、未来可能有用的信息:

# Memory
- API 测试需要本地 Redis 实例。
- Windows 环境下运行 e2e 测试前需要先启动 Docker Desktop。
- 上次调试发现 auth 模块的 token 过期问题来自系统时间偏移。

官方说明 Auto Memory 适合保存 build commands、debugging insights、Claude 发现的 preferences 等内容。

3. 用 SKILL.md

适合封装“任务流程”:

---
name: pr-review
description: Use when reviewing a pull request.
---

# PR Review Process

1. 查看 diff
2. 检查测试覆盖
3. 检查安全风险
4. 输出 review summary

官方建议:如果某个条目是多步骤过程,或者只对代码库某一部分重要,应移动到 skill 或 path-scoped rule,而不是塞进 CLAUDE.md

(九) 优缺点对比

文件优点缺点
CLAUDE.md稳定、明确、适合团队共享太长会占上下文;规则冲突会降低遵守度
MEMORY.md自动学习、减少重复解释可能保存错误或过期知识,需要审计
SKILL.md按需加载,适合复杂任务流程需要设计触发描述;不适合放全局常识

(十) 总结

_**CLAUDE.md**_** 是项目说明书,**_**MEMORY.md**_** 是 Claude
的工作笔记,**
_**SKILL.md**_**
是任务操作手册。三者都是上下文工程的重要组成部分,但都不是硬安全边界;真正需要强制执行的规则应使用 Hooks、权限配置或
CI/CD。**

三、 skills

(一) skills简介

2025年10月16日 Anthropics 最早推出 Skills的概念,并于2025年12月18日 正式将Skills定义为AI开放标准。Anthropics官方文章链接:https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills

skills的作用:AI大模型目前并非全能,对特定领域可能不够了解,直接提问可能得到错误答案。通过Skills功能,用户可为AI提供说明文档(如开发流程、规范或经验),补充AI的知识,使其在需要时查阅,从而提高回答的准确性。例如,可为特定编程语言(如Java、前端开发)或公司内部规范创建Skills文档。参考目前的harness概率,一个skill也能做成专门完成一件特定工作的技能包(比如我个人编写的专门写接口用例的skill:https://github.com/buer2233/api-test-dwp)。

(二) skills的构成

1. Skill存放位置

  • 第一种是项目目录下(项目级)位置是你项目目录下的 .claude/skills/xxx,适合放与你这个项目有关的Skills。

  • 第二种就是用户目录下(全局级),以windows电脑举例:C:\Users\admin.claude\skills,放到这里的Skills所有项目都能用,适合放置一些通用规范和技能。

2. SKILL.md的内容简介

  • 一个skills 必须包括一个 SKILL.md 文件,且其中必须包含两个模块:元数据和指令。元数据 必填两项是:name 和 description。name为SKILL名称,description描述SKILL的作用和触发条件。

skill默认情况下只会加载元数据name和description,在需要使用技能时,才会渐进式的按需加载SKILL内的指令。

3. SKILL.md中的元数据详细介绍

3.1 必填字段

3.2 通用规范元数据(非必填)
  • codex,cursor,vscode等支持skills规范的产品,都支持这些元数据

3.3 Claude Code 额外支持的元数据(非必填)
  • claude code 支持这些元数据,其他codex,cursor等可能不支持或者部分支持

4. Skills 完整目录

  1. SKILL.md和CLAUDE.md 一样,不建议都写到一个文件,SKILL.md 也可以将内容分拆到多个.md文件。
  2. SKILL.md中 支持引入一个脚本,注意一个细节,就是写到Skills里面的脚本,不会加载,而是执行。还有个技巧是针对那种重复性较高,可以脚本化的操作,都建议直接脚本化,以此提升AI执行的效率也能大幅的减少Token的消耗。

(三) skills触发

1. 自动触发

AI会在提问的开始,将skills全局 和 你当前项目下的 skills 元数据内容(name,description 等)都加载一下

所以你什么都不用管,当AI感觉需要读取某个skills的时候,会自动读取。

2. 手动触发

也可以直接通过斜杠指令,来手动触发,根据截图斜杠指令加载的是name的名称,另外也能看到 description的信息。

3. 禁止自动触发

如下图,在SKILL的描述后添加:disable-model-invocation: true。这样skills就不会自动被调用了,只能手动触发了。

(四) Skills中添加参数

1. $ 初步讲解

Claude Code 里跟 $ 有关的写法,其实只有两类:

1.1 参数占位符(接受用户输入的东西)
写法含义
$ARGUMENTS用户传进来的全部内容
$0
$1 $2
第几个
参数(从 0 开始数)
$ARGUMENTS[0]
$ARGUMENTS[1]
上面那种的「全名版」
$名字给参数起个有意义的名字
1.2 环境变量(运行环境自动提供的信息)
写法含义
${CLAUDE_SKILL_DIR}当前 Skill 自己所在的目录
${CLAUDE_SESSION_ID}当前会话的编号

重点划一下: 第一类是 $XXX没有花括号。 第二类是 ${XXX}有花括号
这俩是被两套不同的代码处理的,写错花括号就不会替换,这是新手最容易栽的坑之一。

2. 最基础的 $ARGUMENTS

SKILL.md 正文里直接写 $ARGUMENTS

---
description:分析指定文件
---


请帮我分析这个文件有没有问题:$ARGUMENTS

用户这样调用:

/分析文件 src/login.ts

$ARGUMENTS 就会被替换成用户跟在命令后面写的那串字:

请帮我分析这个文件有没有问题:src/login.ts

就这么简单。 $ARGUMENTS 拿到的是用户在命令名后面写的所有内容

$ARGUMENTS = 「用户输入的所有东西」的占位符。 你把它放哪里,用户的输入就填到哪里。

$ARGUMENTS 必须全大写。写成 $arguments$Arguments 一律不认,不会替换。

3. $ARGUMENTS 自动兜底机制

「如果我忘了写 $ARGUMENTS,用户传的参数是不是就丢了?」

不会丢。

举个例子,假设你的 Skill 长这样,正文里没写任何占位符

请帮我做代码审查。

用户照样传了参数:

/审查 src/login.ts

Claude Code 会发现你正文里没有占位符,于是自动把参数贴到正文末尾,注入给模型的内容会变成:

请帮我做代码审查。


ARGUMENTS: src/login.ts

所以参数永远不会凭空消失。这只是个兜底机制,建议还是显式写 $ARGUMENTS 让效果更可控。

4. 参数切片: $0 / $1 / $2

用编号占位符 $0``$1``$2

任务内容:$0
优先级:$1
负责人:$2

用户调用 /建任务修复登录bug 高优先级张三,结果:

任务内容:修复登录bug
优先级:高优先级
负责人:张三

把参数想象成一排储物柜,编号从 0 开始$0 是第 1 个柜子、 $1 是第 2 个、 $2 是第 3 个……
(程序员数数都从 0 开始,习惯一下就好)

  • 等价写法:

$0 是简写,它的「全名」是 $ARGUMENTS[0]

任务内容:$ARGUMENTS[0]
优先级:$ARGUMENTS[1]

两种写法效果完全一样,平时用 $0 就够了,省事。

5. 给参数起名字: $文件名、 $负责人

第一步:在 SKILL.md 最上面的 frontmatter 里,用 arguments 字段按顺序列出参数名:

---
description:创建任务
arguments:
  -任务内容
  -优先级
  -负责人
---

第二步:正文里就能直接用名字了:

任务内容:$任务内容
优先级:$优先级
负责人:$负责人

用户调用 /建任务修复登录bug 高张三,结果:

任务内容:修复登录bug
优先级:高
负责人:张三

$0 $1 $2 好读太多了。

  • 一个关键认知:名字只是「外号」,本质还是按顺序

这是这一节最重要的一点,很多人会理解错:

❌ 误解:以为可以这样写命令 /建任务负责人=张三优先级=高(像填表格那样指定) ✅ 真相:还是严格按位置对号入座

arguments 里写的第 1 个名字,永远绑定用户传的第 1 个参数

名字只是给「1 号柜子」贴了个标签叫「任务内容」方便你读,但东西还是按顺序塞进柜子的。位置错了,名字救不了你。

  • 容易踩的坑:名字不能用纯数字

别把参数命名成 12 这种纯数字。

因为系统已经用 $1``$2 表示编号了,如果你的参数名也叫 1,两套语法就打架了。系统会直接无视这种命名,相当于没声明。

起名就起有意义的人话,比如 文件名模式负责人

6. ${CLAUDE_SKILL_DIR} 介绍

  • 为什么需要

一个 Skill 经常不只有 SKILL.md 一个文件,旁边可能还放着脚本、模板、参考资料:

我的Skill/
├── SKILL.md
└── scripts/
    └──检查.sh     ←我想在Skill里运行这个脚本

问题来了:怎么写脚本的路径?

如果写死成 /Users/你/.claude/skills/我的Skill/scripts/检查.sh,那别人下载你的 Skill 装到自己电脑就找不到了——每个人电脑的路径都不一样。

  • 怎么用

用魔法变量 **$****{****CLAUDE_SKILL_DIR****}**,它会自动变成「当前 Skill 自己所在的文件夹路径」:

运行检查脚本:
!`${CLAUDE_SKILL_DIR}/scripts/检查.sh`

不管这个 Skill 被装到谁的电脑、哪个项目, ${CLAUDE_SKILL_DIR} 都能准确指向它自己的位置。

${CLAUDE_SKILL_DIR} = 「我这个 Skill 住在哪」。 想引用同目录的脚本/模板,用它就对了,别写死路径

  • 重点:花括号别忘了!

再强调一遍上面提过的:

  • 参数家族(接用户输入的): $ARGUMENTS$0$名字没有花括号
  • 环境家族(运行环境的): ${CLAUDE_SKILL_DIR}${CLAUDE_SESSION_ID}必须带花括号

如果你把环境变量写成 $CLAUDE_SKILL_DIR(少了花括号),它不会被替换。这是新手最常踩的坑之一。

  • 顺便认识一下 ${CLAUDE_SESSION_ID}

${CLAUDE_SESSION_ID} 会被替换成当前对话的编号,常用于给临时文件起不重名的名字,例如:

把结果保存到/tmp/result-${CLAUDE_SESSION_ID}.json

用法和 ${CLAUDE_SKILL_DIR} 一样,记得带花括号。

(五) Skills 和 Plugins 的区别

  • Skills本质上就是一个.md的文件 再加上 其他.md拓展信息 和 拓展脚本
  • Plugin则更全面,里面包括 Skills,hooks,mcp 等信息,可以做更复杂的场景

(六) Skills 和 MCP 的区别

官方原文:https://claude.com/blog/skills-explained

MCP connects s Claude todata; Skills steach Claude what to do with that data.(MCP 适合做获取数据的场景;Skills 更适合做获取数据之后,处理数据的场景)

在这里插入图片描述

(七) Skills的详细执行流程(纯泄露源码分析)

先通过一张图概括整体的执行过程

1. 完整的执行流程图

在这里插入图片描述

2. Skills开始不是全量注入

很多人会以为 Skills 跟 CLAUDE.md 一样,启动会话时就把所有内容塞进上下文。源码里不是这样。

Skills 分两层:

  • 技能清单层:只把 name / description 这类摘要给模型看。
  • 技能正文层:只有模型调用 Skill 工具后,完整 SKILL.md 才会进入上下文。

Skills的提示词在这里 在src/tools/SkillTool/prompt.ts:173

export const getPrompt = memoize(async (_cwd: string): Promise<string> => {
  return `Execute a skill within the main conversation

When users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge.

When users reference a "slash command"or"/<something>" (e.g., "/commit", "/review-pr"), they are referring to a skill. Use this tool to invoke it.

How to invoke:
- Use this tool with the skill name and optional arguments
...
Important:
- Available skills are listed in system-reminder messages in the conversation
- When a skill matches the user's request, this is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task
...
`
                                 })

这段提示词说明了三件事:

  1. 模型要先检查当前任务是否匹配可用 Skills。
  2. 用户提到/command这种 slash command 时,也可能是在说一个 Skill。
  3. 如果 Skill 匹配当前任务,模型应该先调用Skill工具。

所以 Skills 的触发是靠提示词驱动的:AI自我判断

3. 模型为什么知道要调用 Skills

前面提到,Skill 工具的提示词在src/tools/SkillTool/prompt.ts。

这个提示词很重要,因为它给模型建立了一个规则:当用户请求和某个 Skill 匹配时,必须先调用 Skill 工具。

还有一段系统级提示也会补充告诉模型 slash command 和 Skill 的关系,位置在src/constants/prompts.ts:382

hasSkills
  ? `/<skill-name> (e.g., /commit) is shorthand for users to invoke a user-invocable skill. When executed, the skill gets expanded to a full prompt. Use the ${SKILL_TOOL_NAME} tool to execute them. IMPORTANT: Only use ${SKILL_TOOL_NAME} for skills listed in its user-invocable skills section - do not guess or use built-in CLI commands.`
  : null

翻译一下:

/<skill-name> 是用户调用 user-invocable skill 的简写。
执行时,这个 skill 会被展开成完整 prompt。
使用 Skill 工具来执行它。
重要:只对技能列表里的 skills 使用 Skill 工具,不要猜内置 CLI 命令。

用户输入/commit,在 Claude Code 的模型视角里,它并不只是一个普通字符串,而可能是一个需要通过 Skill 工具展开的技能。

4. Skills 是怎么被发现和加载的

技能目录加载的核心函数在:src/skills/loadSkillsDir.ts

它先准备几个路径:

const userSkillsDir = join(getClaudeConfigHomeDir(), 'skills')
const managedSkillsDir = join(getManagedFilePath(), '.claude', 'skills')
const projectSkillsDirs = getProjectDirsUpToHome('skills', cwd)

对应的就是:

用户级:~/.claude/skills
管理级:managed path 下的 .claude/skills
项目级:当前项目路径向上查找 .claude/skills

然后它会并行加载这些来源:也就是说,Skills 的读取不是一个一个串行读,而是多个来源并行读。

const [
  managedSkills,
  userSkills,
  projectSkillsNested,
  additionalSkillsNested,
  legacyCommands,
] = await Promise.all([
  ...
])

5. Skills 的六类加载位置

源码里 Skills 的来源大概有六类:

managed skills
user skills
project skills
plugin skills
bundled skills
mcp skills

// 还有一个旧版兼容来源:legacy commands

具体解释就是

managed:
  getManagedFilePath()/.claude/skills

user:
  ~/.claude/skills

project:
  从当前目录向上查找 .claude/skills

additional:
  --add-dir 指定目录下的 .claude/skills

legacy:
  .claude/commands

plugin:
  插件目录里的 skills

bundled:
  Claude Code 内置注册的 skills

mcp:
  MCP server 暴露的 prompt 型 skills

加载所有命令的入口在src/commands.ts

const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
  const [
    { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
    pluginCommands,
    workflowCommands,
  ] = awaitPromise.all([
    getSkills(cwd),
    getPluginCommands(),
    getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
  ])

  return [
    ...bundledSkills,
    ...builtinPluginSkills,
    ...skillDirCommands,
    ...workflowCommands,
    ...pluginCommands,
    ...pluginSkills,
    ...COMMANDS(),
  ]
})

Skills 最终会进入统一的commands列表。

Claude Code 内部没有把 Skills 和 slash commands 完全割裂开。它们最后都被整理成Command对象,只是来源字段不同。

6. SKILL.md 是怎么被解析的

每个SKILL.md会先经过 frontmatter 解析:src/utils/frontmatterParser.ts:130

export function parseFrontmatter(
  markdown: string,
  sourcePath?: string,
): ParsedMarkdown {
  const match = markdown.match(FRONTMATTER_REGEX)

  if (!match) {
    return {
      frontmatter: {},
      content: markdown,
    }
  }

  const frontmatterText = match[1] || ''
  const content = markdown.slice(match[0].length)
    ...
    }

它只认文件开头这种格式:

---
name: code-review
description: Review code changes
---

# Code Review

...

如果没有 frontmatter,也不是不能加载。源码会返回:

frontmatter: {}
content: markdown

7. Skill被模型调用后发生了什么

Skill 工具真正执行的位置在:src/tools/SkillTool/SkillTool.ts:580

async call(
  { skill, args },
  context,
  canUseTool,
  parentMessage,
  onProgress?,
): Promise<ToolResult<Output>> {
  ...
    }

流程可以概括成:

拿到 skill 名
  ↓
去掉可能存在的 /
  ↓
从 commands 里找到对应 command
  ↓
记录 Skill 使用
  ↓
判断是否 context: fork
  ↓
如果 fork,交给 executeForkedSkill
  ↓
否则走 processPromptSlashCommand
  ↓
返回新的 messages 和 contextModifier

8. SKILL.md 正文展开时会做哪些处理

首先会注入Base directory

位置在src/skills/loadSkillsDir.ts:344:

async getPromptForCommand(args, toolUseContext) {
  let finalContent = baseDir
    ? `Base directory for this skill: ${baseDir}\n\n${markdownContent}`
    : markdownContent
    ...
    }

如果这个 Skill 来自文件系统,有自己的目录,那么展开时会在正文前面加上:

Base directory for this skill: /path/to/.claude/skills/my-skill

因为一个 Skill 不一定只有一个SKILL.md。它的目录里可能还有脚本、模板、示例、参考文件。

加上 base directory 后,模型就知道这个 Skill 的资源根目录在哪里,可以用 Read/Grep 等工具去读同目录下的配套文件。

还会进行如下操作

  • 替换$ARGUMENTS和具名参数
  • 替换${CLAUDE_SKILL_DIR}
  • 替换${CLAUDE_SESSION_ID}
  • 执行!command动态语法

9. Skills 的上下文保存机制

已调用 Skills 会被记录到invokedSkills,状态结构在src/bootstrap/state.ts:1525:

export type InvokedSkillInfo = {
  skillName: string
    skillPath: string
content: string
invokedAt: number
agentId: string | null
}

添加函数在src/bootstrap/state.ts:1534:

export function addInvokedSkill(
  skillName: string,
  skillPath: string,
  content: string,
  agentId: string | null = null,
): void {
  const key = `${agentId ?? ''}:${skillName}`
  STATE.invokedSkills.set(key, {
    skillName,
    skillPath,
    content,
    invokedAt: Date.now(),
    agentId,
  })
}

注意这里会按agentId区分。也就是说,主对话调用的 Skill 和子 agent 调用的 Skill,不会随便混在一起。

10. compact 后为什么还能继续遵守已调用 Skill

Skills 还有一个和 compact 相关的保存机制。当上下文被压缩后,如果已调用 Skill 的正文完全丢失,模型后续就可能忘掉这个 Skill 的规则。

所以 Claude Code 会在 compact 后创建一个invoked_skillsattachment。

生成函数在src/services/compact/compact.ts:1494:

export function createSkillAttachmentIfNeeded(
  agentId?: string,
): AttachmentMessage | null {
  const invokedSkills = getInvokedSkillsForAgent(agentId)

  if (invokedSkills.size === 0) {
    returnnull
  }
    ...
  return createAttachmentMessage({
    type: 'invoked_skills',
    skills,
  })
}

这个 attachment 渲染成模型消息的位置在src/utils/messages.ts:3644:

case 'invoked_skills': {
  if (attachment.skills.length === 0) {
    return []
  }

  const skillsContent = attachment.skills
    .map(
      skill =>
        `### Skill: ${skill.name}\nPath: ${skill.path}\n\n${skill.content}`,
    )
    .join('\n\n---\n\n')

  return wrapMessagesInSystemReminder([
    createUserMessage({
      content: `The following skills were invoked in this session. Continue to follow these guidelines:\n\n${skillsContent}`,
      isMeta: true,
    }),
  ])
}

也就是说,compact 后模型会看到类似:

<system-reminder>
  The following skills were invoked in this session. Continue to follow these guidelines:

### Skill: code-review
Path: projectSettings:code-review

...
</system-reminder>

翻译过来就是:这就是为什么有些 Skill 被调用后,即使发生 compact,后续仍然可能继续生效。

<system-reminder>
  在本课程中调用了以下技能。继续遵循这些准则:

###技能:代码审查
路径:projectSettings:代码审阅

...
</system-reminder>

(八) Skills 源码里的硬性限制

1. /skills/目录不支持单个.md文件

这个限制非常重要。在.claude/skills/目录下,单个.md文件不会被加载。

  • 正确格式:.claude/skills/my-skill/SKILL.md
  • 错误格式:.claude/skills/my-skill.md

源码位置:src/skills/loadSkillsDir.ts:424

2. Skills有上下文预算

预算逻辑在src/tools/SkillTool/prompt.ts:20:

export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000

也就是说,默认技能清单大概占上下文窗口的 1%。

如果上下文窗口是 200k tokens,那么:200000 _* _0.01 = 8000 token

所以默认字符预算就是 8000 token,如果 Skills 太多,清单会被裁剪。

单个 Skill 的描述也有上限。

源码在src/tools/SkillTool/prompt.ts:29:

export const MAX_LISTING_DESC_CHARS = 250

如果description + when_to_use超过 250 字符,就会被截断。这就是为什么SKILL内容写太多之后,AI执行不会全部遵守的原因,因为上下文已经丢失了。

3. MCP Skills 不执行动态 shell 命令

本地 Skills 可以支持:

!`git status --short`

但 MCP Skills 不执行这类动态 shell。这样做是为了安全考虑

因为 MCP skills 是远程来源,源码不信任远程 Markdown 里的 shell 命令。

源码位置:src/skills/loadSkillsDir.ts:371

// Security: MCP skills are remote and untrusted — never execute inline
// shell commands (!`…` / ```! … ```) from their markdown body.

四、 Plugins

Plugin 是一个自包含目录,用来把 Skills、Commands、Agents、Hooks、MCP Servers、LSP Servers、Monitors、Themes 等能力打包起来,然后在个人、项目或团队范围内复用和分发。_ 官方文档也将 plugin 定义为“a self-contained directory of components that extends Claude Code with custom functionality”。_

_官方文档: _https://code.claude.com/docs/zh-CN/plugins-reference

(一) Plugins 基础介绍

1. Plugins 的诞生

如果说 Skills 是一本"专业领域的说明书",那 Plugins 就是一整个"工具箱"

Anthropic 在推出 Skills 之后,发现单纯一个 .md 文件还不够用

有时候用户想要的不只是给 AI 加一段说明,而是希望:

  • 使用特定的AI Agent
  • 设置生命周期 Hook(在操作前后自动执行某段脚本)
  • 接入一个 MCP 服务器(让 AI 能调用外部工具)

把这些零碎东西分开装,又麻烦又容易乱

于是 Plugins 就诞生了——它把上面这些能力打包到一个目录里,一键安装、一键启用、一键禁用

简单理解: Skills 是一本书,Plugins 是一个图书馆

一个 Plugin 里面可以包含很多 Skills,再加上 commands、agents、hooks、MCP 服务等

2. Plugins 解决的核心痛点

我们先来看看,没有 Plugins 之前会有什么问题:

痛点一:能力分散

你想给团队加一套"前端开发规范+自动格式化命令+Hook 自动检查+MCP 接入 Figma",要分别配置 4 个地方

配置完之后,团队里每个新人来都要再配一遍,离谱

痛点二:缺乏分发

就算你写好了一套规范,怎么给别人?发压缩包?发 GitHub?发完别人怎么装到他自己的环境里?

痛点三:版本管理混乱

你改了一个 hook 脚本,团队里有人在用旧版有人在用新版,没人知道谁是对的

Plugins 就是为了一次性解决这些问题

它让你:

  • 把所有能力打包到一个目录里(commands、agents、skills、hooks、mcp、lsp 全都装得下)
  • 通过 Marketplace(插件市场)一键安装和卸载
  • 自带版本号作者信息,方便维护和升级
  • 支持user / project / local 三种作用域,灵活控制对谁生效

举个最直观的例子:

  • 你想让团队用上"浓缩了 10 年经验的 Java 开发规范"——下载一个 java-best-practices 插件就行
  • 你想让 AI 接入公司内部 Jira 系统——下载一个 jira-mcp 插件就行
  • 你想给所有项目加上"提交前自动跑 lint"的 Hook——下载一个 pre-commit-guard 插件就行

(二) Plugins 的构成

1. Plugin 存放位置

1.1 本地缓存目录(全局)

默认是:~/.claude/plugins/(mac/linux)

里面的结构大概是这样:

~/.claude/plugins/
├── known_marketplaces.json   # 已注册的市场列表
├── installed_plugins.json    # 已安装的插件清单
├── marketplaces/             # 各个市场的本地缓存
│   └── claude-plugins-official/
│       └── .claude-plugin/
│           └── marketplace.json
└── cache/                    # 已下载的插件版本缓存
    └── my-plugin@some-market/
        └── 1.0.0/
            └── (插件实际内容)

这是 Claude Code 帮你管理的,你一般不用动手改

1.2 项目级 plugin 目录

放在你的项目下的 .claude-plugin/ 目录里

适合放和这个项目深度绑定的插件源代码

your-project/
└── .claude-plugin/
    └── plugin.json    ← plugin 元数据

如果你只是想"使用"插件,记住安装位置在 ~/.claude/plugins/ 就够了

如果你要"开发"插件,记住 .claude-plugin/plugin.json 这个文件就够了

2. plugin.json

一个 Plugin 必须包含一个 plugin.json 文件,位于插件目录下的 .claude-plugin/plugin.json 路径

它和 Skills 的 SKILL.md 类似,都是用来描述这个东西"是什么、能做什么"

下面是一个最小化的示例:

{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "这是一个演示插件",
  "author": {
    "name": "张三",
    "email": "zhangsan@example.com"
  }
}

只要这 4 个字段,一个最简 Plugin 就成型了

其中 name 是必填的,剩下的都是选填(但是建议都写上,方便别人识别)

plugin.json 里面还可以配置一堆字段,咱们这里先列举一下常用的

3. Plugin 完整目录

光有 plugin.json 还不够,Plugin 真正的"内容"在它的子目录里

一个完整的 Plugin 目录长这样:

my-plugin/
├── .claude-plugin/
│   └── plugin.json        # 必填,插件元数据
├── commands/              # 可选,自定义斜杠命令
│   ├── deploy.md          # /deploy 命令
│   └── format.md          # /format 命令
├── agents/                # 可选,自定义 AI 代理
│   └── reviewer.md        # 代码审查代理
├── skills/                # 可选,插件附带的 Skills
│   └── my-skill/
│       └── SKILL.md
├── hooks/                 # 可选,生命周期 Hook
│   └── hooks.json
├── output-styles/         # 可选,输出风格定义
│   └── concise.md
├── .mcp.json              # 可选,MCP 服务器配置
└── .lsp.json              # 可选,LSP 语言服务器配置

更多推荐