{"slug": "the-portability-trap-when-it-loads-doesn-t-mean-it-works", "title": "The Portability Trap: When 'It Loads' Doesn't Mean 'It Works'", "summary": "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.", "body_md": "I ran a skill migration and nothing broke. Or so I thought.\n\nBreak 3 in my migration diary noted that skills degraded silently. This is the deep dive.\n\nI 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.\n\nHere'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.\n\nTools exist to port skills between agents. [farmage/opencode-skills](https://github.com/farmage/opencode-skills) (111★) bulk-ported 66 Claude Code skills to OpenCode. [crosstrain](https://github.com/fwdslsh/crosstrain) converts Claude Code skills into OpenCode TypeScript plugin tools. [entireio/skills](https://github.com/entireio/skills) (210★) includes a session-to-skill transformation pipeline.\n\nEvery one of them assumes portability — copy the file, map the frontmatter, done. But the [SkillsBench paper](https://arxiv.org/abs/2602.12670) 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.\n\nThe 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.\n\nBefore diving into the audit, here's the math:\n\n`name`\n\n, `description`\n\n, `license`\n\n, `compatibility`\n\n, `metadata`\n\n, and `allowed-tools`\n\n`model`\n\n, `context: fork`\n\n, `hooks`\n\n, `disable-model-invocation`\n\n, and more`name`\n\n, `description`\n\n, `license`\n\n, `compatibility`\n\n, and `metadata`\n\nThat gap is where your skills break. The truly portable subset is: ** name + description + markdown body.** Everything else is harness-specific.\n\nI had six skills in Claude Code. I invoked each one in a fresh OpenCode session to see what happened:\n\n| Skill | Frontmatter beyond portable? | Verdict | Verified in OpenCode |\n|---|---|---|---|\n| brainstorming | No | Port | ✅ Works as-is |\n| skill-creator | No | Port | ✅ Works as-is |\n| gsd | No | Port | ✅ Works as-is |\n| frontend-design | No | Port | ✅ Works as-is |\n| receiving-code-review | No | Port | ✅ Works as-is |\n| morning-brief | Yes — custom dependencies, model pinning | Rebuild | ✅ Rebuilt via session-first method |\n\nFive of six ported cleanly — because they happened to use only the portable subset. The one that didn't (`morning-brief`\n\n) needed model pinning and dependency declarations that OpenCode doesn't read.\n\nThe pattern: **simple skills (name + description + body) port. Skills with harness-specific fields break.**\n\nWhen you move a Claude Code skill to OpenCode, the SKILL.md body transfers intact. The `name`\n\nand `description`\n\ntransfer. But any harness-specific frontmatter field is dropped without a word.\n\nNone of my six skills used fields 1–5 — which is exactly why five of them ported cleanly. Yours probably do.\n\n`allowed-tools`\n\n— safety constraints vanish\nIn Claude Code, `allowed-tools`\n\nrestricts which tools a skill can call. It's a safety boundary.\n\n```\n# Claude Code — skill restricted to read-only GitHub ops\nallowed-tools: mcp__github__list_issues, mcp__github__get_issue\n```\n\nIn 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.\n\nagensi.io's cross-agent compatibility test confirmed this pattern: skills that relied on `allowed-tools`\n\nfor safety constraints lost those constraints when tested across agents. The output was correct, but the boundary was gone.\n\nThere's also a naming problem: Claude Code uses double-underscore MCP tool names (`mcp__github__create_issue`\n\n), while OpenCode uses single-underscore (`github_create_issue`\n\n). Even if OpenCode supported the field, the tool names wouldn't match.\n\n`context: fork`\n\n— isolated execution disappears\nIn Claude Code, `context: fork`\n\nruns the skill as an isolated subagent — separate context window, no contamination of the parent conversation.\n\nIn OpenCode, this field is dropped. The skill body loads into the main context, consuming tokens and polluting the conversation.\n\n`model`\n\n, `hooks`\n\n, `disable-model-invocation`\n\n— cost, automation, and visibility go\nFields 3–5 follow the same pattern: `model`\n\n(pins a skill to a cheap model like Haiku), `hooks`\n\n(before/after/on_error lifecycle), and `disable-model-invocation`\n\n(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`\n\nas an opt-out from the discovery list — not auto-invoke, just hiding from the list.)\n\n`arguments`\n\n— this one crashes\nUnlike the others, `arguments`\n\ndoesn't fail silently. It crashes OpenCode with a `ConfigFrontmatterError`\n\n. Unknown fields are ignored; recognized-but-unsupported fields like `arguments`\n\nare validated and rejected. This is actually the best outcome — a hard error is easier to debug than silent degradation.\n\nOne more practical difference that trips people up:\n\nOpenCode requires `name`\n\nto match `^[a-z0-9]+(-[a-z0-9]+)*$`\n\n— lowercase alphanumeric with hyphens. Claude Code doesn't enforce this.\n\n```\n# Same SKILL.md — works in Claude Code, crashes in OpenCode\nname: Pair Programming\n```\n\nThe name `Pair Programming`\n\nhas a space — valid in Claude Code, invalid in OpenCode. Fix it to `pair-programming`\n\nbefore porting.\n\nCheck your frontmatter. If the name has spaces, uppercase letters, or underscores, fix it before porting.\n\nThere's a second difference that doesn't show up in any frontmatter table: how skills are loaded into context.\n\nIn 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.\"\n\nIn OpenCode, skills are on-demand. The agent sees skill names and descriptions in an `<available_skills>`\n\nXML block, but the full body is never auto-injected. The agent must explicitly call `skill({ name: \"...\" })`\n\nto load the content.\n\nIn 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.\n\nTwo questions get you to the right path.\n\n**Question 1:** \"Does this skill use `allowed-tools`\n\n, `model`\n\n, `context: fork`\n\n, `hooks`\n\n, or `arguments`\n\n?\"\n\n**Question 2:** \"Does an OpenCode primitive replace the field's job?\"\n\nThen follow the answers:\n\n**When:** No harness-specific fields. Name, description, and body only.\n\n**How:** Copy the SKILL.md to `.opencode/skills/<name>/SKILL.md`\n\n(or leave it in `.claude/skills/`\n\n— OpenCode reads both). Verify it appears in the available skills list. Test that the agent calls it when expected.\n\nOne warning: if both `.claude/skills/`\n\nand `.opencode/skills/`\n\ncontain the same skill name, OpenCode may shadow one with the other. Pick a canonical location and delete the duplicate.\n\n**When:** The skill uses `context: fork`\n\n, `allowed-tools`\n\n, or other fields where an OpenCode primitive does the same job.\n\n**How:** The skill probably shouldn't be a skill in OpenCode at all. `context: fork`\n\n→ OpenCode's Task tool (built-in subagent dispatching). `allowed-tools`\n\n→ OpenCode's permission system. `hooks`\n\n→ agent lifecycle hooks. A skill that orchestrates multiple subagents → an OpenCode agent definition. [crosstrain](https://github.com/fwdslsh/crosstrain) demonstrates one approach: converting Claude Code skills into TypeScript plugin tools.\n\n**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.\n\n**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.\n\nI rebuilt my `morning-brief`\n\nskill 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.\n\nThis pattern has community precedent: [Innei/SKILL](https://github.com/Innei/SKILL) (78★) includes a `session-to-skill-and-blog`\n\npipeline that classifies completed engineering sessions into reusable skills and blog posts. The [entireio/skills](https://github.com/entireio/skills) repo (210★) contains a similar transformation skill.\n\n**When:** The skill is niche, harness-specific, or unused in the last 30 days.\n\n**How:** Remove it. Don't migrate dead weight. The audit exists to help you decide what's worth keeping.\n\nYour 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`\n\nfile 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](https://dev.to/buildloops/why-your-coding-agent-keeps-making-the-same-mistakes-agentsmd-fixes-it-4a6b): config is re-declared, not copied.\n\nPartially. OpenCode reads SKILL.md files from `.claude/skills/`\n\nnatively, so the file loads. But OpenCode only recognizes 5 frontmatter fields (`name`\n\n, `description`\n\n, `license`\n\n, `compatibility`\n\n, `metadata`\n\n). Claude Code uses 15+ fields including `allowed-tools`\n\n, `model`\n\n, `context: fork`\n\n, and `hooks`\n\n— all of which are silently dropped by OpenCode. The skill body transfers, but the behavior may change.\n\nOpenCode supports: `name`\n\n, `description`\n\n, `license`\n\n, `compatibility`\n\n, and `metadata`\n\n. Any other frontmatter field is either silently ignored or causes a crash (in the case of `arguments`\n\n).\n\nIt depends on the skill. If your skill uses only `name`\n\n, `description`\n\n, and a markdown body — port it by copying the SKILL.md file. If it uses `allowed-tools`\n\n, `model`\n\n, `context: fork`\n\n, or `hooks`\n\n, 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).\n\nClaude 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>`\n\nlist but must explicitly call `skill({ name: \"...\" })`\n\nto 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.\n\nThe Agent Skills specification ([agentskills.io](https://agentskills.io/specification)) defines 6 frontmatter fields for cross-agent skill portability: `name`\n\n, `description`\n\n, `license`\n\n, `compatibility`\n\n, `metadata`\n\n, and `allowed-tools`\n\n. 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.\n\n*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.*\n\n*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.*\n\nI write about AI engineering stacks, autonomous developer tools, and structural agent design. If you're building in this space, follow ** @buildloops** for weekly breakdowns!", "url": "https://wpnews.pro/news/the-portability-trap-when-it-loads-doesn-t-mean-it-works", "canonical_source": "https://dev.to/buildloops/the-portability-trap-when-it-loads-doesnt-mean-it-works-3a21", "published_at": "2026-08-25 16:12:06+00:00", "updated_at": "2026-08-25 16:44:26.947712+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Claude Code", "OpenCode", "SkillsBench", "farmage/opencode-skills", "crosstrain", "entireio/skills"], "alternates": {"html": "https://wpnews.pro/news/the-portability-trap-when-it-loads-doesn-t-mean-it-works", "markdown": "https://wpnews.pro/news/the-portability-trap-when-it-loads-doesn-t-mean-it-works.md", "text": "https://wpnews.pro/news/the-portability-trap-when-it-loads-doesn-t-mean-it-works.txt", "jsonld": "https://wpnews.pro/news/the-portability-trap-when-it-loads-doesn-t-mean-it-works.jsonld"}}