前情提要
- 插件(Plugin)把 Skill、子代理、Hook、MCP Server 打包在一起,从插件市场一键安装
- 一个真实的例子:社区的 ECC 插件带了几十个 Skill、一批子代理和一整套 Hook,但它的规则(Rules)没法随插件一起装,只能手动复制到
~/.claude/rules/。它的文档里写着一句:「Claude Code plugins cannot distribute rules」- 同一个插件,用插件市场装和手动复制装,二选一,别叠在一起
第9节讲了 Hook,这一节讲把 Hook、Skill、MCP 打包在一起的东西:插件。
两个问题:插件为什么带不了 Rules?一个第三方插件,到底能对你的机器做什么、不能做什么?
代码主要在 src/utils/plugins/。
一、插件就是一个目录

pluginLoader.ts 开头的注释画了一个插件的目录结构。把加载器认识的所有东西列出来,就是这张表:
| 插件里能放 | 放在哪 | 在哪节讲 |
|---|---|---|
| 斜杠指令 | commands/ |
第7节(已经是 Skill 的一种) |
| 子代理 | agents/ |
第12节 |
| Skill | skills/ |
第7节 |
| 输出风格 | output-styles/ |
|
| Hook | hooks/hooks.json |
第9节 |
| MCP Server | .mcp.json,或 plugin.json 里的 mcpServers |
第8节 |
| LSP Server | .lsp.json,或 plugin.json 里的 lspServers |
|
| 设置 | settings.json |
本节第五部分 |
插件没有什么神秘的,就是一个按约定组织的目录,里面放的都是前面几节已经见过的东西。
二、约定优于配置
加载器的逻辑是:先看 plugin.json 里有没有写路径,没写就去找约定好的目录名:
// pluginLoader.ts(简化)
!manifest.commands ? pathExists(join(pluginPath, 'commands')) : false,
!manifest.agents ? pathExists(join(pluginPath, 'agents')) : false,
!manifest.skills ? pathExists(join(pluginPath, 'skills')) : false,plugin.json 甚至可以没有。注释写着:有就读,没有就生成一份默认的(Loaded from plugin.json if present, otherwise creates default)。
所以你打开一个插件的 plugin.json,常常发现里面只有名字、版本、作者几行。没写的那些 Skill、Hook、子代理,都是靠这套约定找到的。
插件不用声明自己有什么,放对目录就行。
三、为什么带不了 Rules

再看一遍第一部分那张表:没有 CLAUDE.md,也没有 rules/。
插件加载完之后,会变成一个 LoadedPlugin 对象(src/types/plugin.ts)。这个类型里有 commandsPath、agentsPath、skillsPath、hooksConfig、mcpServers、lspServers……就是没有任何和规则相关的字段。
加载器里根本没有读规则的代码。 你在插件里放一个 rules/ 目录,它不会报错,只是永远不会被读到。
这其实不算坏事。把插件能带的东西按「占不占上下文」排一下:
| 插件能带的 | 占上下文吗 |
|---|---|
| Skill | 按需:平时只占目录里一行(第7节) |
| 子代理 | 不占:有自己独立的上下文(第12节) |
| Hook | 不占:跑在模型外面(第9节) |
| MCP 工具 | 可以延迟加载(第8节) |
| Rules | 常驻:每次请求都带着 |
插件能带的,都是「按需」或「不占上下文」的东西。常驻的规则每多一条都在占地方,这种东西,让你自己挑着装。
加载器里没有读规则的代码;常驻上下文的东西,不该随插件悄悄装进来。
四、命名空间与两个路径变量
命名空间
插件里的 Skill 和指令,名字前面会加上插件名,代码里是 `${pluginName}:${commandBaseName}`。所以插件的 Skill 叫 插件名:skill名,不会和你自己写的重名。