DEV Community

Build Loops
Build Loops

Posted on

The Portability Trap: When 'It Loads' Doesn't Mean 'It Works'

I ran a skill migration and nothing broke. Or so I thought.

Break 3 in my migration diary noted that skills degraded silently. This is the deep dive.

I copied six Claude Code skills to OpenCode. Every file loaded. Every name appeared in the available skills list. Every description matched. No errors, no warnings, no friction. Weeks later, I noticed things were off — skills behaving differently than expected, models I didn't choose running expensive operations, context bleeding where it shouldn't have. Nothing told me anything was wrong. I had to read the frontmatter side by side to figure out why.

Here's the audit framework I built to prevent that, and the five-minute test that tells you exactly which skills port cleanly and which ones break.


Why the converters miss what matters

Tools exist to port skills between agents. farmage/opencode-skills (111★) bulk-ported 66 Claude Code skills to OpenCode. crosstrain converts Claude Code skills into OpenCode TypeScript plugin tools. entireio/skills (210★) includes a session-to-skill transformation pipeline.

Every one of them assumes portability — copy the file, map the frontmatter, done. But the SkillsBench paper analyzed 47,150 published agent skills and found an average quality score of 6.2 out of 12 — meaning the baseline is already fragile, and silent degradation on top of that compounds fast.

The converters move files. They don't tell you what breaks. One of them — crosstrain — converts skills into TypeScript plugin tools, trading portability for tighter integration. That's a valid choice. But it's a choice the converter made for you, not one you made yourself. That's the gap this post fills.


The portable subset

Before diving into the audit, here's the math:

  • The Agent Skills spec defines 6 frontmatter fields: name, description, license, compatibility, metadata, and allowed-tools
  • Claude Code implements 15+, adding model, context: fork, hooks, disable-model-invocation, and more
  • OpenCode recognizes 5: name, description, license, compatibility, and metadata

That gap is where your skills break. The truly portable subset is: name + description + markdown body. Everything else is harness-specific.


The audit: what I found in my own library

I had six skills in Claude Code. I invoked each one in a fresh OpenCode session to see what happened:

Skill Frontmatter beyond portable? Verdict Verified in OpenCode
brainstorming No Port ✅ Works as-is
skill-creator No Port ✅ Works as-is
gsd No Port ✅ Works as-is
frontend-design No Port ✅ Works as-is
receiving-code-review No Port ✅ Works as-is
morning-brief Yes — custom dependencies, model pinning Rebuild ✅ Rebuilt via session-first method

Five of six ported cleanly — because they happened to use only the portable subset. The one that didn't (morning-brief) needed model pinning and dependency declarations that OpenCode doesn't read.

The pattern: simple skills (name + description + body) port. Skills with harness-specific fields break.


The 5 fields that silently break

When you move a Claude Code skill to OpenCode, the SKILL.md body transfers intact. The name and description transfer. But any harness-specific frontmatter field is dropped without a word.

None of my six skills used fields 1–5 — which is exactly why five of them ported cleanly. Yours probably do.

1. allowed-tools — safety constraints vanish

In Claude Code, allowed-tools restricts which tools a skill can call. It's a safety boundary.

# Claude Code — skill restricted to read-only GitHub ops
allowed-tools: mcp__github__list_issues, mcp__github__get_issue
Enter fullscreen mode Exit fullscreen mode

In OpenCode, this field is dropped. The skill loads and runs, but the tool restriction is gone. If the skill was designed to be read-only, it can now write files.

agensi.io's cross-agent compatibility test confirmed this pattern: skills that relied on allowed-tools for safety constraints lost those constraints when tested across agents. The output was correct, but the boundary was gone.

There's also a naming problem: Claude Code uses double-underscore MCP tool names (mcp__github__create_issue), while OpenCode uses single-underscore (github_create_issue). Even if OpenCode supported the field, the tool names wouldn't match.

2. context: fork — isolated execution disappears

In Claude Code, context: fork runs the skill as an isolated subagent — separate context window, no contamination of the parent conversation.

In OpenCode, this field is dropped. The skill body loads into the main context, consuming tokens and polluting the conversation.

3–5. model, hooks, disable-model-invocation — cost, automation, and visibility go

Fields 3–5 follow the same pattern: model (pins a skill to a cheap model like Haiku), hooks (before/after/on_error lifecycle), and disable-model-invocation (hides a skill from auto-discovery) are all dropped by OpenCode. The skill runs, but cost control, automation, and visibility preferences are gone. (OpenCode V2 beta adds opencode/autoinvoke as an opt-out from the discovery list — not auto-invoke, just hiding from the list.)

6. arguments — this one crashes

Unlike the others, arguments doesn't fail silently. It crashes OpenCode with a ConfigFrontmatterError. Unknown fields are ignored; recognized-but-unsupported fields like arguments are validated and rejected. This is actually the best outcome — a hard error is easier to debug than silent degradation.


The name regex gotcha

One more practical difference that trips people up:

OpenCode requires name to match ^[a-z0-9]+(-[a-z0-9]+)*$ — lowercase alphanumeric with hyphens. Claude Code doesn't enforce this.

# Same SKILL.md — works in Claude Code, crashes in OpenCode
name: Pair Programming
Enter fullscreen mode Exit fullscreen mode

The name Pair Programming has a space — valid in Claude Code, invalid in OpenCode. Fix it to pair-programming before porting.

Check your frontmatter. If the name has spaces, uppercase letters, or underscores, fix it before porting.


The invocation gap nobody mentions

There's a second difference that doesn't show up in any frontmatter table: how skills are loaded into context.

In Claude Code, skills auto-invoke. When your task matches a skill's description, Claude loads the full SKILL.md body into context automatically. You don't ask for it — it just happens. The docs say skills are "automatically invoked when relevant to your task."

In OpenCode, skills are on-demand. The agent sees skill names and descriptions in an <available_skills> XML block, but the full body is never auto-injected. The agent must explicitly call skill({ name: "..." }) to load the content.

In practice, this is less rigid than it sounds — the agent is often fast enough to call the right skill at the right time that it feels automatic. But it's not guaranteed. A skill designed for Claude Code can assume its full body is always in context. A skill designed for OpenCode must work when the agent decides to call it. If a skill isn't being triggered when you expect, this is why.


The decision tree: four paths

Two questions get you to the right path.

Question 1: "Does this skill use allowed-tools, model, context: fork, hooks, or arguments?"

Question 2: "Does an OpenCode primitive replace the field's job?"

Then follow the answers:

Port

When: No harness-specific fields. Name, description, and body only.

How: Copy the SKILL.md to .opencode/skills/<name>/SKILL.md (or leave it in .claude/skills/ — OpenCode reads both). Verify it appears in the available skills list. Test that the agent calls it when expected.

One warning: if both .claude/skills/ and .opencode/skills/ contain the same skill name, OpenCode may shadow one with the other. Pick a canonical location and delete the duplicate.

Convert

When: The skill uses context: fork, allowed-tools, or other fields where an OpenCode primitive does the same job.

How: The skill probably shouldn't be a skill in OpenCode at all. context: fork → OpenCode's Task tool (built-in subagent dispatching). allowed-tools → OpenCode's permission system. hooks → agent lifecycle hooks. A skill that orchestrates multiple subagents → an OpenCode agent definition. crosstrain demonstrates one approach: converting Claude Code skills into TypeScript plugin tools.

Rebuild

When: The skill is worth keeping but its behavior depends on harness-specific fields that don't have a direct convert path — or the skill's value is in its workflow, not its config.

How: Use the session-first method. Do the work manually in OpenCode — the actual task the skill is supposed to automate. Then ask the agent: "Create a skill that does what we just did." The skill emerges from actual behavior, not from porting a config file.

I rebuilt my morning-brief skill this way. I ran a session where I manually did the morning briefing workflow — checking memory, pulling session logs, summarizing open tasks. At the end, I asked OpenCode to create a skill capturing what we'd just did. The new skill worked on the first go — because it was designed for OpenCode's invocation model from the start, not ported from Claude Code's.

This pattern has community precedent: Innei/SKILL (78★) includes a session-to-skill-and-blog pipeline that classifies completed engineering sessions into reusable skills and blog posts. The entireio/skills repo (210★) contains a similar transformation skill.

Delete

When: The skill is niche, harness-specific, or unused in the last 30 days.

How: Remove it. Don't migrate dead weight. The audit exists to help you decide what's worth keeping.


The config principle

Your skills aren't portable files. They're config. And config doesn't copy cleanly between tools — it re-declares. The same SKILL.md file means different things to different harnesses. Copying it without checking frontmatter is like copying a .env file between projects: the syntax is valid, the values are wrong. Same reason your AGENTS.md rules didn't survive a raw copy from Claude Code to OpenCode in post #1: config is re-declared, not copied.


FAQ

Do Claude Code skills work in OpenCode?

Partially. OpenCode reads SKILL.md files from .claude/skills/ natively, so the file loads. But OpenCode only recognizes 5 frontmatter fields (name, description, license, compatibility, metadata). Claude Code uses 15+ fields including allowed-tools, model, context: fork, and hooks — all of which are silently dropped by OpenCode. The skill body transfers, but the behavior may change.

What frontmatter fields does OpenCode support?

OpenCode supports: name, description, license, compatibility, and metadata. Any other frontmatter field is either silently ignored or causes a crash (in the case of arguments).

Should I port or rebuild my Claude Code skills for OpenCode?

It depends on the skill. If your skill uses only name, description, and a markdown body — port it by copying the SKILL.md file. If it uses allowed-tools, model, context: fork, or hooks, either convert it to use OpenCode's equivalent primitives or rebuild it using the session-first method (do the work manually, then ask the agent to create a skill from the session).

Why do my OpenCode skills not trigger automatically?

Claude Code auto-invokes skills when the task matches the skill's description. OpenCode uses on-demand invocation — the agent sees skill names in an <available_skills> list but must explicitly call skill({ name: "..." }) to load the full content. If your skill isn't being triggered, the agent may not be calling it, or the description may not match the task clearly enough.

What is the Agent Skills spec?

The Agent Skills specification (agentskills.io) defines 6 frontmatter fields for cross-agent skill portability: name, description, license, compatibility, metadata, and allowed-tools. It's governed by the Linux Foundation AAIF and adopted by 32+ tools. However, individual agents implement different subsets — Claude Code implements 15+ fields, while OpenCode implements 5.


What broke when you ported skills? Name the field that silently disappeared — and if nothing broke, tell me that too, especially where I'm wrong. I'll collect the rebuilds people reply with in a follow-up.

This is the fifth post in my agent-workflow migration series. Previous: Loaded vs Obeyed: Why Your AI Agent Reads but Doesn't Do. Next: why your agent's harness shapes its behavior more than your prompts do.

I write about AI engineering stacks, autonomous developer tools, and structural agent design. If you're building in this space, follow @buildloops for weekly breakdowns!

Top comments (0)