cd /news/developer-tools/the-portability-trap-when-it-loads-d… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-110523] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

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

A developer's audit of migrating Claude Code skills to OpenCode reveals that skills often degrade silently, with harness-specific frontmatter fields such as allowed-tools and model pinning being dropped without warning. The developer found that only skills using the portable subset of name, description, and markdown body port cleanly, while others require rebuilding. The audit framework and a five-minute test help identify which skills port cleanly and which break.

read10 min views1 publishedAug 25, 2026

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.

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.

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

name

, description

, license

, compatibility

, metadata

, and allowed-tools

model

, context: fork

, hooks

, disable-model-invocation

, and morename

, 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.

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.

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.

allowed-tools

β€” safety constraints vanish In Claude Code, allowed-tools

restricts which tools a skill can call. It's a safety boundary.

allowed-tools: mcp__github__list_issues, mcp__github__get_issue

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.

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.

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.)

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.

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.

name: Pair Programming

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.

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.

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:

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.

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.

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.

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.

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.

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.

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

).

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).

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.

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!

── more in #developer-tools 4 stories Β· sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/the-portability-trap…] indexed:0 read:10min 2026-08-25 Β· β€”