背景
Claude Code と Codex CLI を併用する中で、「片方で作ったスキルを、もう片方でも同じ感覚で使いたい」と思って、symlink で 1ソース化する構成を試していました。経緯はこちらに書いています。
→ Claudeで作ったスキルをCodexでも使えるようにした話 — スキルの二重管理をやめる
その記事で紹介した構成はこんな形でした。
リポジトリroot/
├── skills/ # 本体 (agent-neutral, 1ソース)
│ └── git-sync-update-main/SKILL.md
├── plugins/ # Claude Code 向け wrapper
│ └── git-sync/skills/update-main → ../../../skills/git-sync-update-main
└── codex/ # Codex 向け wrapper
└── skills/git-sync-update-main → ../../skills/git-sync-update-main
agent-neutral な skills/ を root に置いて、両 adapter から symlink で吸う。見た目もスッキリしてるし、新エージェントが増えても adapter を足すだけ。これでいけるはず、と push してマージしたんですが…
起きたこと: /plugin install で skill が認識されない
実際に Claude Code で動かしてみると、/plugin marketplace add t-tonton/tonton-ai-skills も /plugin install git-sync@tonton-plugins も何のエラーも出ない。/reload-plugins してもロード数は変わるけど、肝心の /git-sync:update-main が呼べない。
何かがエラーも出さずに落ちている、と思って中を覗くと原因が見つかりました。
原因: Claude Code は cache 展開で symlink を辿らない
Claude Code は マーケットプレイス取得時 と プラグインインストール時 で、それぞれ別のディレクトリを使います。
- マーケットプレイス取得 →
~/.claude/plugins/marketplaces/<marketplace>/にリポジトリ全体を保存。symlink はそのまま保存される - プラグインインストール →
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/にプラグイン本体だけをコピー。ここで symlink を辿らない
実際にローカルを覗くとこうなっていました。
~/.claude/plugins/marketplaces/tonton-plugins/plugins/git-sync/skills/
update-main -> ../../../skills/git-sync-update-main ← symlink (実体は marketplace 側 skills/ 配下に存在)
~/.claude/plugins/cache/tonton-plugins/git-sync/0.1.0/skills/ ← 空 dir
本体が plugin ディレクトリの 外 (= リポジトリ root の skills/) にあると、cache 展開時にコピーされず、空 dir が残るだけ。Claude Code は cache を読みに行くので、skill が見つからないわけです。
公式ドキュメントの「プラグインキャッシングとファイル解決」にも明記されています。
プラグインがローカルマシンにクローンまたはコピーされると、~/.claude/plugins/cache のローカルバージョン管理プラグインキャッシュにコピーされます。これは、
../shared-utilsのようなパスを使用してプラグインディレクトリの外部のファイルを参照できないことを意味します。これらのファイルはコピーされないためです。
ドキュメントは「外部のファイル」と書いていますが、symlink でその外部を指している場合も同じ扱いになる、というのが今回踏んだ実態でした。
なぜ Codex 側は問題なかったのか
Codex CLI の skill 配置は ~/.agents/skills/ 配下を直接読みに行く方式で、cache を作りません。bash codex/install.sh でリポジトリ内の codex/skills/<flat-name> を ~/.agents/skills/<flat-name> に symlink で配置するだけ。
~/.agents/skills/git-sync-update-main
→ <repo>/codex/skills/git-sync-update-main
→ <repo>/skills/git-sync-update-main
OS レベルで symlink chain を解決できれば実体が見えるので、本体が plugin ディレクトリの外にあっても問題ない。
つまり、Claude Code と Codex で symlink との相性が違う というのが今回の核心でした。
直し方: source-of-truth の向きを反転
Claude Code 側の install を通すには、本体を plugin ディレクトリの 内 に置く必要があります。なら、symlink の向きを反転します。
- 本体を Claude Code が直接読むパス (
plugins/<plugin>/skills/<skill>/SKILL.md) に 実ファイル として置く - Codex 側だけ symlink で吸う
修正後の構造はこうなりました。
リポジトリroot/
├── plugins/
│ └── git-sync/skills/update-main/SKILL.md # ← 実ファイル (本体)
└── codex/
└── skills/git-sync-update-main → ../../plugins/git-sync/skills/update-main
これで /plugin install が cache に本体を持っていけるようになり、Codex 側も symlink chain で本体に辿り着きます。トップ skills/ ディレクトリは廃止。二重管理ゼロは維持できました。
学び: マーケットプレイスを設計するとき
今回の罠から学んだのは:
- SKILL.md 本体は plugin ディレクトリの内側に置くのが原則
- 「外部に置いて symlink で配る」構造は
/plugin installで詰む (silent に失敗するので気づきにくい) - どうしても外出ししたい場合は、CI / release 時に symlink を実体展開する tarball を作る等の工夫が要る
Codex 側のように cache を介さない仕組みなら symlink でも動くので、両エージェントを併用する場合は 本体を Claude Code の規約に揃え、symlink で吸われる側に Codex を回す のがいちばん素直です。
関連
- 元記事 (この罠を踏む前の構成): [Claudeで作ったスキルをCodexでも使えるようにした話 — スキルの二重管理をやめる](TODO: 元記事の Qiita URL)
- 修正後のリポジトリ: https://github.com/t-tonton/tonton-ai-skills
- Claude Code プラグインリファレンス: https://code.claude.com/docs/ja/plugins-reference