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:
---
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.
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 project | Claude Code | Codex | OpenCode |
|---|---|---|---|
.claude/skills/ | Yes | No | Yes |
.agents/skills/ | No | Yes | Yes |
.opencode/skills/ | No | No | Yes |
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
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 -p "/tutorial-review"
codex exec --sandbox read-only '$tutorial-review' < /dev/null
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:
| Runtime | What loading looks like |
|---|---|
| Claude Code | A 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. |
| OpenCode | A 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
| Change | Claude Code | Codex | OpenCode |
|---|---|---|---|
No description | Still works as /tutorial-review, described by its first heading, but Claude didn't choose it for a plain prompt | Skips it, with missing field `description` on standard error only. Exit status 0 | Loads 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 quoted | Works | Works | Drops the skill without any message, even at debug log level |
| An unquoted colon inside the description | Works | Works | Works |
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