📝 摘要:Claude Code、Codex、QoderCN 国际版和国内版,Skill 格式一样但目录各不同,改一条规则要改四处。本文用软链做到「一份维护、处处生效」:真相源只存一份 SKILL.md,各工具 skills 目录 ln -s 指过去。重点是三个反直觉的坑:Codex 项目级读的是中立目录 .agents/skills 而非 .codex/skills;QoderCN 项目级只认项目根 skills/~/.agents/skills 项目层一条顶一片、全局层只覆盖一小组工具。

写在前面

如果你跟我一样,同时跑着四个 AI 编码工具——Claude Code、Codex、QoderCN 国际版、QoderCN 国内版——那你大概率遇到过这个场景:

你写了一条新的 Skill 规则:“Java 内部方法调用必须加 this. 前缀”。

然后你发现:

  • Claude Code 读 ~/.claude/skills/
  • Codex 读 ~/.codex/skills/
  • QoderCN 国际版读 ~/.qoder/skills/
  • QoderCN 国内版读 ~/.qoder-cn/skills/

四个目录,四份文件。你建了四份。

过了两周,你想改一下这条规则的触发描述。你得打开四个文件,改四遍。

又过了一周,你同事问你这条规则到底在哪定义的,你指了指 .claude/skills/。同事说:"但 Codex 好像不认这个路径啊?"你沉默了三秒,默默又建了一条软链。

这就是"配置漂移"——同一个意图散落在多个物理位置,改一处漏两处,时间一长谁也不知道哪份是最新的。

折腾一圈之后,我想通了一件事:这四个工具的 Skill 格式完全相同(都是 SKILL.md + frontmatter),只是配置目录不一样。既然内容一样,为什么不只维护一份,然后用软链把四个目录串起来?

✅ 一条 ln -s:改一条规则只改 1 处

ln -s

ln -s

ln -s

ln -s

ln -s

规则改动

真相源
SKILL.md

~/.claude/skills

~/.codex/skills

~/.agents/skills

~/.qoder/skills

~/.qoder-cn/skills

❌ 复制四份:改一条规则要改 4 处

规则改动

.claude/skills

.codex/skills

.qoder/skills

.qoder-cn/skills

💡 如果你对 Skill 的概念还不太熟悉,建议先看一下 《CLAUDE.md 写到 500 行还管不住 AI?Skills 分层食用指南》,这篇把 Skill 和 CLAUDE.md 的区别讲得很清楚。本文聚焦在"多工具共享"这个环节。

一、问题本质:格式一样,目录不同

先把四个工具的 Skill 目录摆出来(注意是 Skill 目录,不是配置目录,下面会说这个区别有多坑):

工具 全局 Skill 目录 项目级 Skill 目录
Claude Code ~/.claude/skills/ .claude/skills/
Codex ~/.codex/skills/ .agents/skills/
QoderCN 国际版 ~/.qoder/skills/ 项目根 skills/(⚠️ 见下)
QoderCN 国内版 ~/.qoder-cn/skills/ 项目根 skills/(⚠️ 见下)

两个反直觉的点,都是踩过才知道的:

① 全局和项目不是「加个 ~ 」那么简单。 最典型的是 Codex:全局在 ~/.codex/skills/,项目级却不是 .codex/skills/,而是 .agents/skills/——一个跟工具名毫无关系的目录。你按「全局路径去掉 ~」的直觉去建项目级软链,建完不生效,还查不出为什么。

② 「配置目录」和「Skill 目录」是两回事,而且文档不一定对得上实现。 QoderCN 的 settings 之类确实读 .qoder/。公开的规范映射表里,它项目级 Skill 目录也写的是 .qoder/skills/——但我实测放那儿不生效,只有项目根的 skills/ 才被加载(详见 3.2)。

⚠️ 这条值得单独记一笔:这类映射表是社区维护的,未必跟得上每个工具的每个版本。真按表建完不生效,别怀疑人生,直接换个位置试——判据很简单:建完开一个新会话,问它「你现在加载了哪些 skill」,比读任何文档都快。

📌 顺带说,那个 .agents/ 不是 Codex 自创的——它是 Agent Skills 规范 定义的厂商中立目录,一批工具正在往上收敛。这件事对本文方案影响很大,单独放在第四节讲。

好在 Skill 文件的格式完全一样——一个 SKILL.md,frontmatter 里写 namedescription,正文写规则内容。各工具加载的逻辑也一样:扫描自己那个 skills/ 目录,每个子目录就是一个 Skill。

唯一的区别就是目录名。

这就给软链创造了条件:只要把内容放在一个"物理目录"里,然后让四个工具的 skills/ 都指向它,问题就解决了。

二、核心思路:一份物理文件 + 多条软链

~/docs/team-docs/skills/     ← 真相源(单一事实源,唯一维护点)
  ├── common/                ← 全局通用那份
  └── order-svc/             ← 项目专属那份

全局层软链(所有项目生效)
  ├ ~/.claude/skills/        ← Claude Code
  ├ ~/.codex/skills/         ← Codex
  ├ ~/.agents/skills/        ← 中立目录:Cline / Warp / Zed / Kimi Code CLI
  ├ ~/.qoder/skills/         ← QoderCN 国际版
  └ ~/.qoder-cn/skills/      ← QoderCN 国内版

项目层软链(仅本仓库生效)
  ├ <project>/.claude/skills/  ← Claude Code
  ├ <project>/.agents/skills/  ← 中立目录:Codex / Cursor / Gemini CLI / Copilot / OpenCode…
  └ <project>/skills/          ← QoderCN(项目根,两版本共用)

一句话总结:SKILL.md 共享一份,各工具的 README / settings 各自维护。

为什么不全量软链(把整个 .claude/ 链过去)?因为每个工具还有自己的配置文件——Claude Code 有 CLAUDE.md,QoderCN 有 settings.json,这些东西不该共享。只软链 skills/ 这一层,粒度刚好。

三、实操:两层软链,全局 + 项目

Skill 分两层——全局层(所有项目生效)和项目层(仅当前项目生效)。软链也要分两层建。

3.1 全局层

全局层放在用户主目录下,所有项目都会加载:

# 真相源只有一份,公共层放在:~/docs/team-docs/skills/common/

# Claude Code
mkdir -p ~/.claude && ln -s ~/docs/team-docs/skills/common ~/.claude/skills

# Codex
mkdir -p ~/.codex && ln -s ~/docs/team-docs/skills/common ~/.codex/skills

# QoderCN 国际版
mkdir -p ~/.qoder && ln -s ~/docs/team-docs/skills/common ~/.qoder/skills

# QoderCN 国内版
mkdir -p ~/.qoder-cn && ln -s ~/docs/team-docs/skills/common ~/.qoder-cn/skills

# 中立目录(Cline / Warp / Zed / Kimi Code CLI 这一组全局读它)
mkdir -p ~/.agents && ln -s ~/docs/team-docs/skills/common ~/.agents/skills

建完之后,这几个路径都指向同一个物理目录。你在里面新增一个 Skill,对应的工具同时生效。

⚠️ 全局层没有「一条通吃」的写法。 ~/.agents/skills 听着像万能钥匙,但它在全局层只覆盖 Cline、Warp、Zed、Kimi Code CLI 这一组;Codex 全局读的是 ~/.codex/skills/、Cursor 是 ~/.cursor/skills/、Gemini CLI 是 ~/.gemini/skills/——各建各的,一条都省不了。真正能「一条顶一片」的是项目层(见 3.2 和第四节)。

3.2 项目层

项目专属的 Skill 同样收在真相源里——~/docs/team-docs/skills/ 下的项目子目录(如 order-svc),各项目再软链接入。

这里有个和全局层不一样、且实测踩过的坑:项目级的 QoderCN 不认 .qoder/skills.qoder-cn/skills 这种放在 dot 目录里的软链(全局层放 ~/.qoder/skills 是认的,项目层却不认)。项目级 QoderCN 只认项目根目录下的 skills/。所以项目层的 QoderCN 只需在项目根建一条 skills 软链,国际版、国内版就都生效了:

# 真相源:~/docs/team-docs/skills/order-svc(order-svc 项目专属那份)

# Claude Code:项目级读 <project>/.claude/skills,指过去
ln -s ~/docs/team-docs/skills/order-svc ~/repos/order-svc/.claude/skills

# Codex 及一大批工具:项目级读中立目录 <project>/.agents/skills(不是 .codex/skills!)
mkdir -p ~/repos/order-svc/.agents && \
  ln -s ~/docs/team-docs/skills/order-svc ~/repos/order-svc/.agents/skills

# QoderCN(国际版 + 国内版共用这一条):软链建在项目根,名字就叫 skills
ln -s ~/docs/team-docs/skills/order-svc ~/repos/order-svc/skills

💡 上面第二条是本文更新过的写法。我最早按「全局 ~/.codex/skills → 项目级 .codex/skills」的对称直觉建,建完 Codex 根本不加载。查了规范才知道,Codex 项目级读的是中立目录 .agents/skills/——而且不只 Codex,Cursor、Gemini CLI、GitHub Copilot、OpenCode 等一批工具项目级全都读这个路径。这条软链的性价比因此高得离谱:一条顶一片

⚠️ 别在项目里建 .qoder/skills / .qoder-cn/skills——实测不生效。项目级 QoderCN 只扫项目根的 skills/;一条软链两个版本共用,比全局层还省一条。(全局层没有"项目根"这一说,仍按 3.1 给 ~/.qoder~/.qoder-cn 各建一条。)

3.3 一个完整的软链清单

以下是我实际在用的完整清单,涵盖全局层和多个项目层,供参考:

# 真相源(唯一维护点):~/docs/team-docs/skills/{common,order-svc,dw-platform,card-keeper}

# ── 全局层:各工具各挂一条,都指向 common(这一层省不了)──
ln -s ~/docs/team-docs/skills/common ~/.claude/skills      # Claude Code
ln -s ~/docs/team-docs/skills/common ~/.codex/skills       # Codex
ln -s ~/docs/team-docs/skills/common ~/.agents/skills      # Cline / Warp / Zed / Kimi
ln -s ~/docs/team-docs/skills/common ~/.qoder/skills       # QoderCN 国际版
ln -s ~/docs/team-docs/skills/common ~/.qoder-cn/skills    # QoderCN 国内版

# ── 项目层:每个项目三条即可 ──
#   .claude/skills  → Claude Code
#   .agents/skills  → Codex / Cursor / Gemini CLI / Copilot / OpenCode… 一条顶一片
#   skills(项目根)→ QoderCN 国际版 + 国内版共用
for p in order-svc dw-platform card-keeper; do
  SRC=~/docs/team-docs/skills/$p
  mkdir -p ~/repos/$p/.claude ~/repos/$p/.agents
  ln -s "$SRC" ~/repos/$p/.claude/skills
  ln -s "$SRC" ~/repos/$p/.agents/skills
  ln -s "$SRC" ~/repos/$p/skills
done

⚠️ ln -s 前必须确保目标位置不存在。如果目标已经是个目录(哪怕是空的),ln -s 不会替换它,而是在它里面建一个同名软链,变成 skills/skills——而且不报错。建完一律 ls -l 看一眼箭头,别只看命令没报错就以为成了。

四、.agents/skills:正在收敛的中立标准

写这篇的时候我发现一件事,值得单独说:这个「每家一个目录」的乱局,正在被一个中立标准收拾

Agent Skills 规范 定义了一个不属于任何厂商的目录——.agents/skills/。摘一段各工具的实际映射(完整表在 vercel-labs/skills):

工具 项目级 Skill 目录 全局 Skill 目录
Codex .agents/skills/ ~/.codex/skills/
Cursor .agents/skills/ ~/.cursor/skills/
Gemini CLI .agents/skills/ ~/.gemini/skills/
GitHub Copilot .agents/skills/ ~/.copilot/skills/
OpenCode .agents/skills/ ~/.config/opencode/skills/
Cline / Warp / Zed / Kimi Code CLI .agents/skills/ ~/.agents/skills/
Claude Code .claude/skills/ ~/.claude/skills/
QoderCN 国际版 / 国内版 .qoder/skills/(⚠️ 实测不生效,见 3.2) ~/.qoder/skills/~/.qoder-cn/skills/

盯着这张表看,会发现一个很有意思的分裂:

  • 项目层已经基本统一了:Codex、Cursor、Gemini CLI、Copilot、OpenCode、Cline、Warp、Zed……项目级清一色 .agents/skills/
  • 全局层还各玩各的:同样这批工具,全局路径五花八门(~/.codex/~/.cursor/~/.gemini/~/.copilot/~/.config/opencode/),只有 Cline、Warp、Zed、Kimi Code CLI 这一组真正落到了中立的 ~/.agents/skills/

收敛是从项目层开始的——这跟直觉相反(你会以为全局配置更容易统一),但想想也合理:项目目录是团队共享的,一个仓库里塞七八个 .xxx/skills 谁都受不了;而全局目录是个人机器上的事,各家没动力改。

对本文方案的实际影响:

# 项目层:一条 .agents/skills 顶一大片工具,优先建这条
mkdir -p ~/repos/order-svc/.agents && \
  ln -s ~/docs/team-docs/skills/order-svc ~/repos/order-svc/.agents/skills

# 全局层:中立目录只覆盖 Cline / Warp / Zed / Kimi 这一组,其余仍需各建各的
ln -s ~/docs/team-docs/skills/common ~/.agents/skills

⚠️ 别把 ~/.agents/skills 当成全局层的万能钥匙。 我一开始就理解错了,以为建了它 Codex 全局就通了——实际 Codex 全局读的是 ~/.codex/skills/,那条还得单独建。中立目录在项目层是「一条顶一片」,在全局层只是「多一个工具组」。

顺带一提,既然有了统一规范,就有了配套的包管理器:skills CLI 能直接从 GitHub 装社区写好的 Skill,装完各工具通用。这也带来一个坑,放在第七节讲。

五、.gitignore 怎么处理软链

软链建好之后,有个容易踩的坑:Git 会把软链本身当作一个文件来跟踪(存储的是链接目标路径字符串),而不是跟踪它指向的目录内容。

如果你不想把软链提交到仓库(比如它是本地开发环境的私有配置),需要在 .gitignore 中排除它。

5.1 软链能被 .gitignore 排除吗?

能。 Git 对软链的处理是把它当作一个普通的 blob 文件,.gitignore 的规则可以正常匹配。

# 排除项目级软链(项目根的 skills;若历史上还留着 .codex/.qoder/.qoder-cn 旧链一并排除)
/skills
.codex/skills
.qoder/skills
.qoder-cn/skills

5.2 如果软链已经被 Git 跟踪了怎么办?

需要先从索引中移除,再加 .gitignore

# 从索引中移除(不删除物理文件)
git rm --cached skills

# 然后加 .gitignore
echo "/skills" >> .gitignore

💡 git rm --cached 只从 Git 索引中移除,不会删除磁盘上的软链。放心用。

5.3 另一种思路:把软链也提交进去

如果你的团队都使用相同的工具组合,也可以把软链提交到仓库里。这样其他人 git clone 下来就自动有了正确的软链结构。不过要注意:

  • 软链的目标路径必须是相对路径(否则换个人机器上就断了)
  • 或者用脚本在初始化时动态创建(见下一节)

六、新增 Skill 的完整流程

有了软链之后,新增一条 Skill 的流程变得非常简单:

  1. 判断归属:这条 Skill 是全局通用的,还是项目专属的?
  2. 在物理目录创建mkdir -p {物理目录}/{skill-name}
  3. 编写 SKILL.md:frontmatter 填 namedescription,正文写规则
  4. 完成——四个工具自动生效,不需要额外操作

6.1 SKILL.md 的编写注意

由于多个工具共享同一份 SKILL.mddescription 中应使用通用表述,不要写死某个工具的名字

# 推荐 ✅
description: |
  TRIGGER when 执行 mvn / gradle / javac 编译时...

# 不推荐 ❌
description: |
  让 Claude 在执行 mvn 编译时自动...

这样无论哪个工具加载这条 Skill,描述都是准确的。

6.2 软链完整性检查

真正容易漏的是全局层——每个工具一条、路径还各不相同,漏建一条你只会发现"某个工具没加载规则",但很难第一时间想到是软链没建。项目层反而省心:三条固定写法,其中 .agents/skills 一条覆盖一大批。

一个自检脚本,全局层逐条查、项目层查三个位置:

check() {  # $1=路径
  if [ -L "$1" ]; then
    tgt=$(readlink "$1")
    [ -e "$1" ] && echo "✅ $1$tgt" || echo "💀 死链: $1$tgt(目标不存在)"
  elif [ -d "$1" ]; then
    echo "⚠️  $1 是真实目录,不是软链(八成是 ln -s 嵌套了,查查里面有没有 skills/skills)"
  else
    echo "❌ 缺: $1"
  fi
}

# 全局层:各工具路径不同,一条都不能少
for l in ~/.claude/skills ~/.codex/skills ~/.agents/skills ~/.qoder/skills ~/.qoder-cn/skills; do
  check "$l"
done

# 项目层:每个项目三条
for dir in ~/repos/*/; do
  d="${dir%/}"
  check "$d/.claude/skills"    # Claude Code
  check "$d/.agents/skills"    # Codex / Cursor / Gemini CLI / Copilot…
  check "$d/skills"            # QoderCN(项目根)
done

比原来多了两种状态判断:死链(软链在、目标没了,readlink 照样有输出,容易误判成正常)和真实目录(多半是 ln -s 嵌套的后果,见 7.1)。只判断 -L 会把这两种都放过去。

七、踩坑记录

7.1 ln -s 嵌套创建

现象:软链不生效,ls -la 发现 ~/repos/order-svc/skills 变成了一个真实目录,里面还嵌了一个叫 skills 的软链。

原因:建软链时 ~/repos/order-svc/skills 已经存在(可能是个空目录),ln -s 不会覆盖,而是在这个目录里面创建了一个名为 skills 的软链。

解决:先删掉已有的空目录,再建软链:

rm -rf ~/repos/order-svc/skills   # 确认是空目录 / 失效软链再删!
ln -s ~/docs/team-docs/skills/order-svc ~/repos/order-svc/skills

7.2 软链目标路径用绝对路径 vs 相对路径

绝对路径的好处是直观、不容易搞错层级;坏处是换台机器或者目录搬家就断了。

相对路径的好处是仓库可以整体迁移;坏处是层级关系搞错了就 404。

我的选择:全局层用绝对路径(因为 ~ 展开后每个人的路径本来就不同,全局层本来就是本地配置),项目层也用绝对路径(配合脚本初始化,不依赖相对层级)。

如果你希望仓库可移植,项目层建议用相对路径:

# 相对路径写法(项目根 skills)
ln -s ../../docs/team-docs/skills/order-svc skills

7.3 删除物理目录后软链变"死链"

现象:某个工具突然不加载 Skill 了,ls -la 发现软链指向的目标不存在了。

原因:物理目录被删除或重命名了,但软链没有同步更新。

排查

# 找到所有死链
find . -type l ! -exec test -e {} \; -print

解决:重新指向正确的物理目录,或者删除死链。

7.4 装社区 Skill,装进了自己的真相源仓库

有了统一规范就有了包管理器:skills CLI 能从 GitHub 拉别人写好的 Skill 装到本地。我顺手装了两个,然后 git status 一看愣住了——它们出现在我的真相源仓库里,还被 git 跟踪了

原因~/.agents/skills 是一条指向真相源的软链。CLI 往「~/.agents/skills/」写文件,写的其实是软链背后那个物理目录——也就是你的仓库。软链是双向透传的,你从软链读,别人也能从软链写进去。

这不是 bug,得看你要什么

  • 想要:三方 Skill 一并版本化、多工具通用、换机器 clone 就有 → 保持现状,但在 README 里标出哪些是三方的,免得半年后看见没印象的目录顺手删了。
  • 不想要:那就别把中立目录指向仓库,改成物理目录 + 仓库软链进去(反向挂),或者干脆 .gitignore 掉三方那几个目录。

还有一条:三方 Skill 的安装台账在 ~/.agents/.skill-lock.json(记来源仓库、SKILL.md 路径、内容哈希)。要卸载走 CLI,别手动 rm -rf 目录——删了目录但台账还在,下次 CLI 检查状态就对不上了。

💡 顺带说:这也是「只软链 skills/ 这一层、别整个 .agents/ 链过去」的另一个理由——.skill-lock.json 这种工具自己管的状态文件,本来就不该进你的仓库。

八、方案对比:软链 vs 其他方案

可能有人会问:为什么不直接用其他方案?简单对比一下:

方案 优点 缺点
软链(本文方案) 零依赖、即时生效、改一处全生效 需要理解软链机制;IDE 偶尔不跟随软链
复制文件 最简单粗暴 改一条改 N 处,必然漂移
脚本同步 可以跨机器 需要定时任务或手动触发,有延迟
统一配置目录 最优雅 需要工具本身支持自定义配置路径(目前不支持)

软链方案的核心优势是零维护成本——建好之后就不用管了,新增/修改/删除 Skill 都是操作物理目录,软链自动透传。

总结

四个 AI 编码工具,四份配置目录,一份 Skill 内容——用一条 ln -s 就能把它们串起来,告别复制粘贴和配置漂移。

核心就四句话:

  1. 物理目录只存一份 SKILL.md,这是唯一的维护点
  2. 全局层每个工具各建一条软链,路径互不相同、一条都省不了(~/.claude/skills~/.codex/skills~/.agents/skills~/.qoder/skills~/.qoder-cn/skills
  3. 项目层只要三条.claude/skills 给 Claude Code、.agents/skills 一条顶一片(Codex / Cursor / Gemini CLI / Copilot / OpenCode…)、项目根 skills/ 给 QoderCN 两个版本共用
  4. .gitignore 可以正常排除软链,不想提交就加一行规则

如果只让我留一句话,是这个:「全局路径去掉 ~ 就是项目路径」这个直觉是错的。Codex 全局在 ~/.codex/skills、项目级却在 .agents/skills;QoderCN 全局在 ~/.qoder/skills、项目级却在项目根 skills/。这两处不对称,是我在这套方案上浪费时间最多的地方——软链本身五秒就建好了,难的是知道该往哪儿建。

如果你也在多工具之间反复同步配置,不妨试试这个方案。毕竟,改一条规则只需要改一个地方,才是工程师该有的生活。


延伸阅读


🏷️ 标签AI 编码 Claude Code Codex Agent Skills 软链 配置管理

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐