Home › Learn › Skills across vendors

Tutorial · 10 min read · Claude Code · Codex · OpenCode

One skill for Claude Code, Codex and OpenCode: where each looks, and how to tell it loaded

Claude Code, Codex and OpenCode all read skills written as a SKILL.md file, but each looks in different folders and treats the frontmatter differently. We gave all three the same code-review skill and a change with a real bug. Each one found the bug. Getting there taught us which folders to use, why the frontmatter has to be valid YAML, and why a correct answer doesn't prove the skill loaded.

The skill and the bug

A skill is a folder named after the skill, holding a SKILL.md file: YAML frontmatter with a name and a description, then instructions in Markdown. Vendor docs This is the file we used, unchanged, in all three runtimes:

tutorial-review/SKILL.md
---
name: tutorial-review
description: Review the uncommitted working-tree change in this git repo for bugs. Use when asked to review my change, my diff, or my working tree.
---

# Tutorial review

1. Run `git diff` to see the working-tree change. Read the changed files if you need more context.
2. Report each real bug you find as `file:line` followed by one sentence on what goes wrong and one concrete input that shows it.
3. Suggest exactly one regression test (a short function body) that fails on the current change and passes once the bug is fixed.
4. Do not edit, create, or delete any files. Review only.
5. Start your answer with the line `tutorial-review skill loaded`.

Step 5 is there so you can see the instructions were followed. As the rest of this page shows, that's a different thing from the runtime loading the skill.

The test repository has a stock counter that must never go below zero, and an uncommitted change that breaks it. The existing tests still pass with the bug.

git diff
diff --git a/inventory.py b/inventory.py
index 7a60650..e121b66 100644
--- a/inventory.py
+++ b/inventory.py
@@ -3,7 +3,8 @@
 
 def remove_stock(on_hand: int, sold: int) -> int:
     """Return the new on-hand count after a sale. Never below zero."""
-    return max(0, on_hand - sold)
+    new_count = on_hand - sold
+    return new_count
 
 
 def restock(on_hand: int, received: int) -> int:

Where each runtime looks

Folder in the projectClaude CodeCodexOpenCode
.claude/skills/YesNoYes
.agents/skills/NoYesYes
.opencode/skills/NoNoYes

We tested each folder on its own for each runtime. Observed Codex also looks in the parent folder and at the repository root, and each runtime has a folder in your home directory for personal skills. Vendor docs

So one copy in .claude/skills/ and a symlink to it in .agents/skills/ reaches all three. Codex followed the symlink. Observed

Terminal, at the repository root
mkdir -p .agents/skills
ln -s ../../.claude/skills/tutorial-review .agents/skills/tutorial-review

OpenCode then sees the skill twice and logs duplicate skill name. When we kept three separate copies, which one OpenCode used changed from run to run: over twelve runs it picked the .claude copy five times, .agents four times and .opencode three times. Observed With a symlink the copies can't drift apart, so that doesn't matter. With real copies it will, as soon as someone edits one.

Run it

Claude Code
claude -p "/tutorial-review"
Codex
codex exec --sandbox read-only '$tutorial-review' < /dev/null
OpenCode
opencode run "Use the tutorial-review skill on this repo" < /dev/null

All three named the changed line in inventory.py, gave an input that goes negative, such as remove_stock(3, 5) returning -2, and suggested one regression test. None edited a file. Each run took between 9 and 21 seconds. Observed

Asked "Review my working-tree change" without naming the skill, all three picked it up from its description. Observed In our non-interactive shell, opencode run sometimes hung with no output until we redirected standard input from /dev/null, and codex exec prints Reading additional input from stdin... without it. Observed

OpenCode needs a model that can call tools. A local 7B model we tried wrote the skill call out as text instead of making it, so the skill never loaded. Observed

How to tell it really loaded

With the .agents copy moved away, Codex had no skill to load. The model searched the repository, found the .claude copy of SKILL.md, read it with cat, and printed a normal-looking review. Observed The answer alone can't tell you which happened. These can:

RuntimeWhat loading looks like
Claude CodeA slash command is expanded into the prompt, with no tool call. The transcript shows the command, then the skill body and a Base directory for this skill line. When Claude picks the skill itself, you see a Skill tool call and Launching skill: tutorial-review. With --output-format stream-json, the start-up event lists the skill under skills.
Codex--json output has no skill event for $tutorial-review. The session file under ~/.codex/sessions/ shows the skill injected as a <skill> block with its path. When Codex picks the skill itself, it reads the file with cat, so that step is visible in --json.
OpenCodeA skill tool call titled Loaded skill: tutorial-review, whose output names the folder it came from. opencode debug skill lists what it found without calling a model.

Each row is what we saw with the versions above. Observed

Frontmatter that works everywhere

We broke the frontmatter on purpose in each runtime. Observed

ChangeClaude CodeCodexOpenCode
No descriptionStill works as /tutorial-review, described by its first heading, but Claude didn't choose it for a plain promptSkips it, with missing field `description` on standard error only. Exit status 0Loads it only when asked by name. The model's list of skills leaves it out, with no warning
Claude's argument-hint: [file] [focus] added, not quotedWorksWorksDrops the skill without any message, even at debug log level
An unquoted colon inside the descriptionWorksWorksWorks

The argument-hint row is about YAML, not the field. A value that starts with [ is read as a list, so [file] [focus] without quotes is a syntax error. Claude Code and Codex loaded the skill anyway, and OpenCode skipped it. Observed Quoting the value, as in argument-hint: "[file] [focus]", fixed it. After we quoted it in three skills that had the unquoted form, OpenCode listed all three, and Claude Code still listed them as skills and slash commands. Observed

Fields OpenCode doesn't know are fine once the YAML is valid. The same three skills also carry Claude's disable-model-invocation, context and agent fields, and OpenCode listed them. Observed Its documentation names four fields, name, description, license and compatibility, and requires the name to be lowercase words joined by hyphens, matching the folder name. Vendor docs

Because OpenCode skips a broken skill without a word, check with opencode debug skill, which prints every skill it found as JSON. On our Mac, piping that output cut it off at about 64 KB, so write it to a file before searching it. Observed

Instructions in a skill are requests to the model. "Do not edit files" held in our runs, but the runtime's own permissions and sandbox are what stop an edit. We ran Codex with --sandbox read-only for that reason.

What we haven't verified

  • The desktop apps. We ran all three runtimes in a terminal.
  • Personal skills in the home-directory folders, and Codex's repository-root lookup from a subfolder.
  • OpenCode with a local model strong enough to call tools. Not verified

Last verified 2026-09-25 · review by 2026-10-25