投稿

Claude Code の skills を codex CLI からも読ませる(そして Claude が黙って skip していた SKILL.md を炙り出す)

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 などをローカルで検出できるようにした

Claude Code をメインに使いつつ codex CLI を並行運用しているのだが、両ツールで書いた skill を二重管理するのはナンセンスだった。Claude 側の ~/.claude/skills/ には blog 投稿、PR 作成、ユビキタス言語などのスキルが 90 本以上ある。同じことを ~/.codex/skills/ にコピペしたい気持ちはゼロ。

まず調べたのは SKILL.md のフォーマットの互換性で、両ツールとも冒頭に --- で YAML frontmatter を置き、namedescription を必須級で読む点は共通していた。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/claude

dotfiles で管理しているなら 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 を追加した瞬間に落ちる方が発見が早いのは間違いない。

トレンドのタグ