Skip to content

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.

Four things you keep teaching the agent, all currently crammed into the rules file. Each needs a home: the rules file (loaded every session), a skill (a 25-token menu line every session, full body only when invoked), or the prompt (said when it comes up). Re-home them and watch the ledger.

  • cents, not floatsfact · matters most sessions · ~22 tok

    Money is integer cents - amountCents: number, never floats.

    right home

    Twenty-two tokens a session and the float bug is extinct. A fact the agent must hold while writing any code has to be in view before it starts - that’s the rules file.

  • release checklistprocedure · runs ~2×/week · ~1.2k tok

    The release ritual: bump the version, regenerate the changelog, tag, build, smoke-test - fifteen steps in a fixed order.

    idle rent

    Fifteen steps loaded into every session to be used twice a week. The other eight sessions pay 1.2k tokens for a procedure that never runs - bulk that competes with your real rules for attention.

  • migration recipeprocedure · runs ~2×/month · ~900 tok

    Schema migrations follow expand → backfill → contract, with a fixture check and a written rollback plan.

    idle rent

    Twenty sessions pay 900 tokens each for every one that migrates. The rarer the procedure, the worse a rules file suits it.

  • today’s flakeone-off · this week only · ~25 tok

    Today’s job: chase the flaky auth test in login.spec.ts.

    rent for a one-off

    True this week, noise forever after. Next month the agent still “knows” about a flake that’s long fixed.

window cost21.5k tok/weekyou re-teach0×/weekmisplaced3 of 4

The procedures are paying rent in every session for the few that use them. Move them behind a skill stub: the knowledge stays one invocation away, and the window cost collapses.

Numbers are illustrative - the ratios are the point. Over 10 sessions a week: a rule pays full size every session, a skill pays a ~25-token stub plus its body only when it fires, a prompt pays only when said - but you’re the one saying it.

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.

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 Reject

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

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.