在 monorepo 里同时维护根目录与子项目两套名为 deploy 的技能时,Claude Code 采用「目录限定名共存」的策略:根的 deploy 与子项目目录下的 deploy 同时可用,裸输入 /deploy 仍走根技能,输入 /子项目目录:deploy 才会明确调用子项目版本;让 Claude 自动选时,它会按”当前正在改动的文件所在目录”匹配最贴合的同名变体。把这套机制说清楚,是把 deploy 拆开重用的前提。
一、为什么子项目与项目根需要同名 deploy
把根 .claude/skills/deploy 留作通用发布流水线,把 apps/web/.claude/skills/deploy 写成只针对前端 bundle 的版本,是 monorepo 团队的常见诉求:根版本要兼顾多语言构建与多环境切换,web 子项目版本则只关心静态资源上传与 CDN 刷新。如果强行只保留一份,要么把 web 流程挤进通用脚本、配置条件分支越来越脏,要么让 web 团队每次手动 cd 进子目录再唤起技能。Claude Code 自 v2.1.203 起引入嵌套 .claude/skills 目录发现机制,就是为这类”一份根脚本 + 多份子项目脚本”场景提供原生支持。
二、三种调用方式的差异
子项目与项目根同时存在 deploy 时,三种调用路径对应三段不同的执行链路:
| 调用方式 | 实际加载的技能 | 适用场景 |
|---|---|---|
| 裸名 /deploy | 根 .claude/skills/deploy(始终) | 跑整库通用发布、CI 触发 |
| 限定名 /apps/web:deploy | 子项目下的 deploy | 明确只想发 web 端 |
| 让 Claude 自主判断 | 根技能 + 目录限定变体清单,Claude 自行追加”工作目录”对应的变体 | 改到 web 文件时希望它自动切到 web 版本 |
2.1 裸名仍走根
无论 Claude 当前在编辑哪个子目录,裸 /deploy 永远命中根 .claude/skills/deploy。Claude Code 把这种”跨层级同名技能”视为两个独立条目,而非覆盖。
2.2 限定名显式指定
输入 /apps/web:deploy 时,Claude Code 按子目录路径拼出限定名,只加载那一份。这条路径与 cwd 无关——只要子项目 .claude/skills 存在且命名匹配,就能从根会话直接调用。
2.3 自主追加匹配变体
调用裸名 /deploy 时,Claude Code 加载根版本,并在上下文中追加一段指令:”如有其他目录限定变体对应的文件正被处理,请同时调用该变体。”于是根脚本里”通用步骤”与子项目脚本里”web 专属步骤”会按当前编辑文件所在子目录自动联动。
三、加载时机与发现机制
Claude Code 对不同位置 .claude/skills 的发现时机并不一致,理解这一点能避免”为什么我新加的技能没生效”的反复排查:
| 位置 | 加载时机 | 备注 |
|---|---|---|
| 启动目录的 .claude/skills | 会话启动时 | 必加载 |
| 启动目录到仓库根的父级 .claude/skills | 会话启动时 | 自动向上遍历 |
| 启动目录以下的嵌套 .claude/skills | 按需,首次读到该子目录文件时 | 懒加载 |
嵌套目录的”懒加载”是 monorepo 友好性的关键:会话从根启动并不会立刻把 web / backend / infra 几十个子项目的技能全塞进上下文,而是等到 Claude 真正改动 web/src/Nav.tsx 时才把 web 的技能拉进来。上下文体积可控,跨包干扰也少。
四、典型 monorepo 目录结构
下面是一个按”公共服务集中 + 子项目自治”思路组织技能的最小骨架:
acme-monorepo/
├── .claude/
│ └── skills/
│ ├── deploy/ # 通用发布:版本号写入、tag 推送、Slack 通知
│ └── review/ # 通用 review:lint、类型检查、单元测试
├── apps/
│ ├── web/
│ │ └── .claude/
│ │ └── skills/
│ │ └── deploy/ # web 专属:next build + CDN 刷新
│ └── backend/
│ └── .claude/
│ └── skills/
│ └── deploy/ # 后端专属:镜像构建 + k8s rollout
└── infra/
└── .claude/
└── skills/
└── deploy/ # 基础设施:Terraform plan/apply
把通用流程(版本号写入、tag 推送)放根,把”做哪个平台的事”放子项目,技能之间靠目录前缀互相区分、不再争抢同一名字。
五、让同名 deploy 共存的落地步骤
落地时按下面五步把”同名技能不打架”的约束写进仓库与协作流程:
- 先把根 .claude/skills/deploy/SKILL.md 收敛为只做跨子项目的通用动作(版本号、tag、通知),任何”只对单一子项目有意义”的步骤一律下沉;
- 在每个需要专属流程的子项目下新建 .claude/skills/<同名>/SKILL.md,文件名与根保持一致,让限定名机制生效;
- 子项目脚本里只写本项目工具链相关命令(如 web 目录里只允许
next build与cdn-cli refresh),不要再去调用其他子项目的发布逻辑; - 在 CLAUDE.md 或 AGENTS.md 的”技能索引”小节注明”deploy 同时存在于根与子项目,调用约定见上表”,让团队成员不必每次靠经验;
- 每次新增子项目技能时,跑一次裸名 /deploy 验证根脚本仍可独立完成,根技能不能因为新增子项目技能而失效。
5.1 父级同名技能与子项目同名技能冲突时的处理
如果根 .claude/skills/deploy 与 apps/web/.claude/skills/deploy 描述过于接近,模型会”猜不准”。推荐用 description 字段做语义切割:根版本里写”用于跨子项目通用发版前准备”,web 版本里写”用于 web 前端构建并刷新 CDN”。描述越具体,Claude 在自动追加变体时越不容易串。
5.2 跨层级覆盖与目录内共存是两套规则
容易踩坑的一点:跨层级(企业 / 个人 / 项目根)的同名技能是”高位覆盖低位”,被覆盖的一方直接不可见;而项目根与子项目目录是”地图”而非”层级”,两者共存、各自加限定名。把这套心智模型记清楚,就不会误以为”子项目版本会覆盖根版本”。
六、常见误区
第一个误区是把根脚本写成”超级脚本”,把所有子项目分支都塞进去;这会让根脚本膨胀、阅读成本上升,也失去了”子项目脚本”存在的意义。第二个误区是限定名拼错:限定名是”相对仓库根的子目录路径 + 冒号 + 技能目录名”,少了路径或多一层 .. 都会让 Claude Code 找不到目标。第三个误区是新增顶层 .claude/skills 后期待”立即生效”——这种顶层目录的变更需要重启会话,已在运行的会话不会重新扫描。
到这里,根与子项目同名 deploy 的共存机制、调用方式与目录骨架就完整了。下一步只要把”通用动作留在根、子项目专属动作下沉”这条规则贯彻下去,monorepo 的发布脚本就能保持精简。
常见问题(FAQ)
Q1:/deploy 一定走根版本吗?
是。Claude Code 始终把根 .claude/skills 下的 deploy 视为默认条目,裸名调用只命中根。
Q2:apps/web:deploy 这种限定名规则是按仓库根还是按 cwd?
按仓库根。子目录路径是从仓库根算起的相对路径,cwd 不影响限定名的解析。
Q3:新增顶层 skills 目录后修改会立即生效吗?
不会。顶层目录的变更需要重启会话才能被监控;嵌套目录下的改动是热生效的。