Course · OpenCode · Skills
Control which agents can run a skill
A packaged skill - a SKILL.md file under .opencode/skills/<name>/, whose frontmatter name and description the agent sees up front - is not a passive document. The add-a-feed skill for feedmill - the course’s illustrative Go feed aggregator - tells the agent to fetch a sample of the feed over the network, write a parser adapter under internal/feed/, register the source, and round it off with a fixture test that runs go test ./internal/feed/.... That’s exactly right for your build agent - OpenCode’s default coding agent, the one that edits and runs commands. It’s the wrong procedure for the auditor you spin up to review feedmill’s parser code: a subagent you define in .opencode/agents/auditor.md and invoke by @-mentioning it, as @auditor, whose job is to read and report.
Two different settings shape that auditor, and it pays to keep them apart. Its edit, bash, and task permissions are the control here, the rules OpenCode enforces on the tools themselves: set to deny, they block file writes, shell commands, and handing work to another agent, whatever the agent has read. The skill permission decides which procedures can enter the auditor’s context - the instructions it has in view when it decides what to do. A skill body is guidance, not a capability; denying it doesn’t stop a curl that bash allows, and allowing it doesn’t let the agent past a bash deny. Gate the skill anyway, because a fetch-write-test procedure in a reviewer’s context is at best noise and at worst an instruction that pushes it toward actions its permissions should refuse.
The reason this is even a question - and why it has a clean answer - is how skills get into an agent. Their bodies aren’t text stapled into the system prompt. Only the available skill names and descriptions are listed up front (in the system prompt and in the skill tool’s description, as of October 2026, v1.18.32, per session/system.ts), and the agent calls the one built-in skill tool to load a skill’s body on demand, only when it decides it needs that procedure. That on-demand loading is what makes skills gateable. A load that goes through a tool is a load you can scope per agent, the same way you scope edit or bash.
Gate by name with permission.skill
Section titled “Gate by name with permission.skill”OpenCode permissions are per-tool and per-agent, and the skill tool gets its own map keyed by skill name. You write glob patterns that resolve to allow, deny, or ask:
# .opencode/agents/auditor.md frontmatter (excerpt; the full file also sets description)permission: edit: deny bash: deny task: deny skill: "*": deny "audit-*": allowThe equivalent opencode.json form nests the agent definition under agent:
{ "agent": { "auditor": { "permission": { "edit": "deny", "bash": "deny", "task": "deny", "skill": { "*": "deny", "audit-*": "allow" } } } }}The edit, bash, and task lines keep the auditor from writing files, running commands, or delegating. Read the skill map the way OpenCode reads it: deny everything, then carve back the audit family. The add-feed skill matches *, hits deny, and never appears as something the auditor can load - while an audit-parser-coverage skill stays available. The skills documentation says denied skills are hidden and rejected; permission patterns are order-sensitive, with the last matching rule winning, so write the catch-all first and specific exceptions after it (permissions documentation, as of October 2026; worked examples in the permissions chapter). Flip the polarity when the agent is broadly trusted and you only want to fence off a few procedures, again in the trusted agent’s own frontmatter:
# .opencode/agents/trusted.md frontmatterpermission: skill: "*": allow "deploy-*": deny "rotate-*": askNow the agent can load any skill except the deploy ones, and has to stop and ask you before loading anything that rotates credentials. The globs match against the skill’s name field - deploy-* covers deploy-staging and deploy-prod without you listing each one. Note the patterns are simple */? wildcards over the skill name, not recursive ** path globs; you’re matching names, not directory trees.
You can watch the gate hold. Tell the gated agent to do the thing the skill would handle, and instead of silently loading it, OpenCode refuses the skill tool call - a deny rule rejects the load and hides the skill from the agent entirely (per the skills documentation, a denied skill is “hidden from agent, access rejected”, as of October 2026). The transcript below is illustrative - the verified behavior is the rejected load, not this exact wording:
@auditor onboard the new Reuters feed using the add-feed skill
⏵ skill add-feed permission denied: skill "add-feed" is not allowed for this agent
I can't load the add-feed skill here. Edits and shell commands are also denied for this agent, so I can't write the adapter or run go test. I can review the existing adapters, and you can run the skill from build.The load failed because the deny rule sits under the skill tool, not inside the skill. The skill folder is untouched and still works perfectly from build.
Turn the skill tool off entirely
Section titled “Turn the skill tool off entirely”Per-name globs are the right reach when an agent should keep some skills. When an agent should have none - a pure reviewer, a docs-only scout - don’t enumerate denials. Switch the whole skill tool off:
---description: Review code without loading procedures that can act on the repo.mode: subagentpermission: edit: deny bash: deny task: denytools: skill: false---
Review the requested code and report findings. Do not edit files or runproject procedures.With the tool itself disabled, there is no skill surface to gate at all - the agent can’t load any skill because the mechanism that loads them isn’t in its toolbox. When the skill tool is off, OpenCode omits the available-skills section (<available_skills>) from the agent’s context entirely (skills documentation, as of October 2026). This is the blunter move, and it fits a reviewer that should pull in no procedure at all. You’re not maintaining a deny-list that a newly-added skill could slip past; you’ve removed the door to procedures. The door to acting is a separate one: the edit, bash, and task denials in the same frontmatter are what keep the reviewer from writing files or running commands.
One caveat on form: the tools map is documented but deprecated in favor of permission - the agents documentation says “tools is deprecated. Prefer the agent’s permission field for new configs,” and the permissions documentation notes the legacy tools boolean was deprecated as of v1.1.1 and “is still supported for backwards compatibility” (both as of October 2026). So tools: { skill: false } keeps working and stays the clearest way to express “no skill tool at all,” but if you prefer to stay on the non-deprecated surface, "permission": { "skill": { "*": "deny" } } reaches the same end - every skill denied, the <available_skills> section gone - as long as "*": "deny" is the last matching skill rule; an allow for any name after it brings the section back (permission source, as of October 2026). The kill-switch framing holds either way.
The split is worth holding onto. permission.skill is for which skills an agent may load; tools.skill: false is for whether the agent may load skills at all. Reach for the glob map when you’re curating a subset, and the kill switch when the answer is “obviously none.”
Why this matters for feedmill specifically
Section titled “Why this matters for feedmill specifically”The gate matters more as feedmill’s procedures grow: a future rotate-sync-token skill would walk the agent through touching the sync server’s credentials. A reviewer that already denies *, like the auditor above, blocks it with no new rule; an agent that allows skills by default needs a rotate-* rule, as in the trusted agent’s map. The procedure stays one load away for build and out of the reviewer’s context.
That closes the Skills chapter: you can package a procedure once, carry it between tools, and decide per agent who’s allowed to load it. Notice what you just leaned on to make the gate hold, though - a second agent, scoped differently from the one you work in. That second agent is useful beyond permissions: work you hand off to a separate agent runs in its own context, so the noisy reading never lands in your thread at all. Next chapter: Subagents.