Skip to content

Control which agents can run a skill

You packaged the add-a-feed procedure as a skill two lessons ago, and it does what skills are supposed to do — it shells out. It runs feedmill add, hits the network to fetch the feed once, writes a fixture, runs go test ./internal/ingest/.... That’s exactly right for your build agent. It’s exactly wrong for the read-only auditor you spin up to review feedmill’s parser code, where the whole point is that the agent can’t touch disk or run commands. The problem isn’t that the auditor would misuse the skill. It’s that the skill existing in the auditor’s toolbox quietly undoes the read-only guarantee you set up the agent to have.

The reason this is even a question — and why it has a clean answer — is how skills get into an agent. They aren’t text stapled into the system prompt. OpenCode exposes every skill through one built-in skill tool: the agent sees the available skill names and descriptions, and calls the 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 capability the agent reaches for through a tool is a capability you can scope per agent, the same way you scope edit or bash.

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, or opencode.json under `agent`
{
"permission": {
"skill": {
"*": "deny",
"audit-*": "allow"
}
}
}

Read that 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. Evaluation is last-match-wins, the same as the rest of OpenCode’s permission system, so the catch-all goes first and the specific exceptions go after it. Flip the polarity when the agent is broadly trusted and you only want to fence off a few procedures:

{
"permission": {
"skill": {
"*": "allow",
"deploy-*": "deny",
"rotate-*": "ask"
}
}
}

Now 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 docs, a denied skill is “hidden from agent, access rejected”). 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 — this agent is scoped read-only.
I can describe the steps the skill performs so you can run it from your
build agent, but I won't run feedmill add or write the fixture myself.

The auditor stayed inside its lane because the deny rule sits under the skill tool, not inside the skill. The skill folder is untouched and still works perfectly from build.

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:

// auditor.md frontmatter
{
"tools": {
"skill": false
}
}

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 from the agent’s context entirely. This is the blunter, stronger move, and it’s the honest one for an agent whose entire identity is “reads, never acts.” You’re not maintaining a deny-list that a newly-added skill could slip past; you’ve removed the door.

One caveat on form: the tools map is documented but deprecated in favor of permission — the agents docs say tools is deprecated. Prefer the agent’s permission field for new configs,” and the permissions docs note the legacy tools boolean “is still supported for backwards compatibility.” 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. 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”

feedmill’s skills are not passive lookups — onboarding a feed runs the binary and the network, and a future rotate-sync-token skill would touch the sync server’s credentials. Those are precisely the procedures you want your acting agent to reach for by name and your reviewing agent to be structurally unable to invoke. Gate them once, in the auditor’s frontmatter, and the read-only promise you make when you @auditor something is actually enforced by OpenCode rather than relying on the agent to behave. The same procedure stays one fetch away for build and impossible for the reviewer — which is exactly the separation you wanted when you split the agents in the first place.

That separation only holds if the skill travels with the right shape no matter which tool’s directory it lives in. Next: reuse skills across tools.