Claude Code の skills を codex CLI からも読ませる(そして Claude が黙って skip していた SKILL.md を炙り出す)
まとめ
- Claude Code と codex (OpenAI Codex CLI) は SKILL.md のフォーマットがほぼ互換なので、
~/.codex/skills/claude → ~/.claude/skillsの symlink 1本で codex 側から Claude の skills が全部見えるようになる - 移行時に codex が「skipped 5 skills」と警告を出したことで、Claude 側では静かに一覧から欠落していただけの壊れた SKILL.md(frontmatter 欠落、YAML パース失敗)が炙り出された。Claude は破損を無視するタイプ、codex は明示 skip するタイプ
- codex の skill 起動は Claude と別体系。
/skill-nameの slash 呼び出しは動かず、model が description マッチで暗黙起動するか、$skill-nameで明示起動する - 恒久対策として
scripts/validate-skill.pyを skill.knowledge に同梱し、descriptionの未 quote colon などをローカルで検出できるようにした
symlink 1本で共有する
Claude Code をメインに使いつつ codex CLI を並行運用しているのだが、両ツールで書いた skill を二重管理するのはナンセンスだった。Claude 側の ~/.claude/skills/ には blog 投稿、PR 作成、ユビキタス言語などのスキルが 90 本以上ある。同じことを ~/.codex/skills/ にコピペしたい気持ちはゼロ。
まず調べたのは SKILL.md のフォーマットの互換性で、両ツールとも冒頭に --- で YAML frontmatter を置き、name と description を必須級で読む点は共通していた。codex 側のフィールド(metadata.short-description 等)と Claude 側のフィールド(when_to_use, allowed-tools, argument-hint)は違うが、片方が知らないフィールドは無視されるだけで壊れない。
codex は ~/.codex/skills/** を再帰的に discovery する。純正 skill が ~/.codex/skills/.system/ にサブディレクトリで入っていることからも、ネストが効くのは確認できた。ならば Claude の skills ディレクトリをまるごと symlink すればいい。
mkdir -p ~/.codex/skills
ln -sfn ~/.claude/skills ~/.codex/skills/claudedotfiles で管理しているなら install スクリプトに置くのが確実:
# scripts/install-dotfiles-linux.sh
ln -sfn "$HOME/.claude/skills" "$HOME/.codex/skills/claude".system/ 以下の codex 純正 skill と共存できるので副作用はない。SKILL.md の実体は 1 個だけで両ツールから読まれる。
codex が炙り出してくれた「静かに壊れていた」SKILL.md 5 本
symlink を張って codex を起動したら、いきなり警告が出た。
⚠ Skipped loading 5 skill(s) due to invalid SKILL.md files.
⚠ .../git-create-branch/SKILL.md: missing YAML frontmatter delimited by ---
⚠ .../create-instruction/SKILL.md: invalid YAML: did not find expected key at line 2 column 105, while parsing a block mapping
⚠ .../git.create-issue-pr/SKILL.md: missing YAML frontmatter delimited by ---
⚠ .../git.update-pr/SKILL.md: missing YAML frontmatter delimited by ---
⚠ .../mcp-management/SKILL.md: missing YAML frontmatter delimited by ---うち 4 本は frontmatter そのものが無く、# 見出し から始まっていた。Claude Code は frontmatter 欠落を許容してディレクトリ名を name にフォールバックするため、これらは今まで「なんとなく動いていた」。ただし description が読めない以上、Claude の自動発火判定は破綻していたはずで、手動 /コマンド名 でしか呼べない状態だった(本人はそう認識していない)。
もう 1 本 create-instruction は YAML の構文エラーで、description の中身が原因だった:
# NG(ダブル外囲みの中にダブルクォートが入り、column 105 で YAML パーサが破綻)
description: "他の Claude セッションに作業を委任するための指示書を作成する。対象リポジトリ・作業内容・出力先を引数に取る(例: /create-instruction myapp "cap タスク有効化" phases/phase2/)。"
# OK(外囲みをシングルに切り替えれば、中のダブルはリテラルとして通る)
description: '他の Claude セッションに作業を委任するための指示書を作成する。対象リポジトリ・作業内容・出力先を引数に取る(例: /create-instruction myapp "cap タスク有効化" phases/phase2/)。'YAML は shell と違って \" エスケープが効かないので、外囲みを切り替えるのが最短。
Claude は破損 SKILL.md を無言で読み飛ばす。codex は skip した名前を stderr に並べる。移行を機に壊れているものが 5 本見つかったのは棚ぼただった。書いた本人が気づけていなかったわけで、Claude だけで運用していたら永遠に発見できなかった気がする。
呼び出し方は Claude と別体系
symlink で見えるようになった blog.publish を codex から /blog.publish と叩いたら怒られた。
• Unrecognized command '/blog.publish'. Type "/" for a list of supported commands.codex 純正 skill の references を追うと、policy.allow_implicit_invocation: true(既定)なら model context に skill が注入され、description マッチで自動選択される。明示指定するときは $skill-name を使う、という設計だった(openai_yaml.md より)。
- Claude:
/blog.publishの slash 起動が第一義 - codex: 自然文で頼めば model が description で選ぶ。明示は
$blog.publish。/は CLI 組み込みコマンド専用
同じ SKILL.md でも呼び出し UI は別物だと思っておいた方がいい。Claude で書いた「TRIGGER when: ブログ投稿、blog 書いて」のような when_to_use は、codex 側でも description の補足として model に届くので書き方の流用は効く。
Claude が黙るなら validator を持つ
同じことが今後も起きうる。恒久対策として SKILL.md を機械チェックする validator を skill.knowledge に同梱した。
python3 ~/.claude/skills/skill.knowledge/scripts/validate-skill.pyチェックしているのは 5 項目で、severity を 2 段階に分けている。ERROR は両ツールで load 失敗する系(frontmatter delimiter 欠落、閉じる --- 欠落、YAML パース失敗、description 欠落)、WARN は Claude は許容するが codex や agentskills.io 準拠ツールで問題化する系(name 欠落、name とディレクトリ名の不一致)。
初回スキャンでは 104 skills 中 1 個の追加 ERROR(description に含まれる ASCII colon : を quote し忘れて YAML パース失敗)と 22 個の WARN が拾えた。この 1 個の ERROR も Claude 上では静かに欠落していたやつだ。CI にかけたければ --warn-as-error を付ければ WARN も exit=1 になる。
依存は PyYAML だけなので Ubuntu なら大抵入っている。実装は 100 行程度で、frontmatter を抜き出して yaml.safe_load() に食わせて mapping と name/description を見るだけの素直なコード。壊れていても静かに欠落するタイプのツールは、外から明示的に叩く道具を用意しない限り自力では気づけない。今回の一番の学びはそこだった。
一般化できる持ち帰り
- フォーマット互換のツールが 2 つあるなら、まず片方をもう片方に symlink して繋げるのが最短だった。コピペで運用すると更新が片方に寄り、忘れた頃に片方が古くなる。SoT を 1 つにするのが結局はいちばん楽
- 警告を出すツールに、警告を出さないツールの成果物を食わせるのは、実質的にタダの lint になる。異なる実装を並べる意味はここにある
- 静かに壊れる系は、外部から叩く lint を持たないと恒久的に気づけない。気づく契機が誰かの指摘しかないなら、その穴は運用に残り続ける
未検証で残っている点も併せて置いておく。codex の $skill-name 明示起動が dot 入り名前(blog.publish)で通るかは確かめていない。通らなければ hyphen 別名を作ることになる。もう一つは、validator を dotfiles の make ci に組み込むか検討中。skill を追加した瞬間に落ちる方が発見が早いのは間違いない。