Course · OpenCode · Skills
Package the add-a-feed procedure as a Skill
You’ve walked OpenCode through onboarding a new feed source to feedmill - the course’s illustrative Go feed aggregator - three times now: sniff the format, write the adapter, register it, add the fixture test. Each time you typed the same four-step narration with a different feed plugged in. The retyping is pure overhead. The steps are stable; only the feed changes. So you write the procedure down once, in a form the model can load when it is relevant, and you stop being the thing that remembers the routine.
That form is a Skill: a directory with a SKILL.md in it. The frontmatter tells the model when the skill may apply; the body tells it what to do. OpenCode lists every skill’s name and description in its built-in skill tool and loads the body only on invocation - when the model decides your request matches one, it may call that tool to pull the body in and follow it (as of October 2026; skills documentation). You never need to paste the procedure again, but review the resulting tool calls as usual.
It’s worth pausing on why this beats the other homes the procedure could have. The four steps could sit in AGENTS.md - the rules file OpenCode reads in full on every session - but you onboard a feed maybe twice a week, so the other sessions would carry those steps for nothing. A skill inverts the cost: the name and description stay in view for tens of tokens, and the body loads only when a feed actually shows up. That stub-always, body-on-invocation split is the skill’s whole economy - and it is exactly why the description you write below does the entire selection job, because the agent never sees the body before it decides. The ledger below makes the case with four examples from a different codebase, each tried in three homes. Its release checklist is the same kind of knowledge as feed onboarding: an occasional multi-step procedure.
Where the file goes
Section titled “Where the file goes”A project skill lives at .opencode/skills/<name>/SKILL.md. OpenCode finds project skills by walking up from your current working directory until it reaches the git worktree - the top of your checkout - so a skills directory anywhere on that climb is discovered (as of October 2026; skills documentation). Put it in the repo and it travels with feedmill for anyone who clones it. If you wanted the skill on every repo you touch - a personal habit, not a project one - the global path is ~/.config/opencode/skills/<name>/SKILL.md instead. We want this one in the repo, so:
> create the directory .opencode/skills/onboard-feed/
| Bash mkdir -p .opencode/skills/onboard-feed/The directory name matters more than it looks. The name field in the frontmatter must match the directory that contains SKILL.md - onboard-feed here. OpenCode also constrains the name itself: 1-64 characters, lowercase alphanumeric with single hyphens between words, no leading or trailing hyphen and no --. So onboard-feed is legal; Onboard_Feed or onboard--feed would be rejected. Pick the directory name and the name field together and keep them identical.
Write the SKILL.md
Section titled “Write the SKILL.md”Now the file. Two required frontmatter fields, then the body. Describe what you want to OpenCode in prose - your prompt supplies the four steps, and the agent reads the repo to fill in paths - and review what it writes. One note first, because the exchange shows something a stock config would not: build is OpenCode’s default coding agent, and out of the box its edits land without asking, but this exchange runs against a repo whose opencode.json (OpenCode’s config file) sets permission.edit to ask - a gate that holds every agent’s file edits behind an approval prompt, build included. That is why the file write below asks before it lands (permissions documentation, as of October 2026). An illustrative exchange:
> write .opencode/skills/onboard-feed/SKILL.md. It's the procedure we keep doing by hand: onboarding a new feed source. Frontmatter with name and a description that says when to use it. Body is the four steps - sniff the format, write the parser adapter, register it, add a fixture test.
| Write .opencode/skills/onboard-feed/SKILL.md
--- name: onboard-feed description: >- Onboard a new RSS/Atom/JSON feed source into feedmill. Use when the user wants to add a feed, or when a new source needs a parser adapter. Produces a registered adapter pinned by a fixture test under testdata/. ---
# Onboard a new feed source
Run these four steps in order for the feed URL the user gives you.
1. **Sniff the format.** Fetch a sample of the feed. Decide whether it's RSS, Atom, or one of our JSON-feed shapes, and find where it puts the id, title, published timestamp, and link. 2. **Write the adapter** in `internal/feed/` that maps this feed's quirks onto feedmill's internal item struct. Reuse an existing adapter as a template; don't invent a new shape. 3. **Register the adapter** in the source-to-adapter map so the sync server reaches for it when it sees this source. 4. **Add a fixture test.** Drop the sample in `testdata/`, assert the adapter produces the expected items, and run `go test ./internal/feed/`.
Permission required Allow once Allow always RejectRead the frontmatter before you approve. The name matches the directory. The description is doing the real work here - it’s the only thing OpenCode sees when it’s deciding whether to reach for this skill, because the body stays unloaded until the skill tool fires. A vague description like “feed stuff” means the agent never connects “add this feed” to this procedure. A good one names the trigger (“add a feed”, “new source needs a parser adapter”) and sketches the payoff, in the 1-1024 characters the field allows. Write it for the agent’s selection problem, not as a title.
Those two fields are all you must supply. OpenCode recognizes a small set of optional ones too - license, compatibility, and a metadata string map - and ignores anything else. There’s no disable-model-invocation or context: fork key of the kind another tool’s skill format uses; OpenCode’s frontmatter is just these fields, and a skill folder authored for Claude Code loads here unchanged - OpenCode reads .claude/skills/ (and .agents/skills/) directly, and any extra keys those tools wrote get dropped rather than erroring (as of October 2026; skills documentation). A dropped key also drops the behavior it controlled, so check any imported skill that relied on one; the page on reusing skills across tools covers what changes.
Let the model choose it when relevant
Section titled “Let the model choose it when relevant”Here’s the payoff. A new feed shows up - a podcast that publishes a JSON feed - and you say the thing you’d say to a teammate, with no mention of the procedure. The model can match your request against the skill descriptions it is holding, choose onboard-feed, and call the skill tool to load the body. The same edit gate as before still holds - each parser edit lands behind your approval - so read the diffs as they come. An illustrative exchange:
> add this source to feedmill: https://example.com/pod/feed.json
| Skill onboard-feed loaded: Onboard a new feed source (4 steps)
| Bash curl -s https://example.com/pod/feed.json | head -40
This is JSON Feed 1.1 - items[].date_published is RFC 3339, items[].url is the link, items[].id is the guid. internal/feed/json.go already maps those fields (JSONItem, PublishedAt), so I'll reuse it instead of writing a new adapter. I'll add the source to the adapter map in internal/feed/registry.go, then add a fixture test.
| Edit internal/feed/registry.go → approved | Write testdata/example_pod.json → approved | Write internal/feed/json_test.go → approved
| Bash go test ./internal/feed/ ok github.com/example/feedmill/internal/feed 0.197s
Done. Registered example.com/pod against the existing JSON Feed adapter and pinned it with a fixture test.Read that first beat. You never said “use the onboard-feed skill” - the model chose it from the description and pulled the body in with the skill tool, the same way it reaches for read or bash. From there it followed your four steps, in your order, with the fixture test you’d otherwise have to remember to ask for. It also adapted one step: this feed matches a shape feedmill already parses, so step 2 became a reuse of the JSON Feed adapter instead of a new file. A skill body is guidance that the model applies to the repo it finds. Check each deviation against the skill, and update the body when the repo changes. The procedure that lived in your head, and that you’d narrated three times, now lives in the repo and is available on recognition.
The same approval model still applies inside the skill: the body is instructions, not a bypass. Each edit and each bash command the skill drives still goes through the permission you’ve set for the current agent - so if you’re on an agent with edit: "ask", you’ll approve the adapter edit exactly as you would any other. A skill makes the agent remember the routine; it doesn’t make it trusted to run it unwatched. That gating is its own lever, and Control which agents can run a skill covers turning it up and down.
One procedure, written once, now owned by the tool. The next time you add a feed you describe the source and read the diff - the four-step dance is OpenCode’s job. The next page examines how far the shared Agent Skills format travels across tools that read the same skill paths; tool-specific gating and frontmatter still need separate verification. Next: reuse skills across tools.