Course · OpenCode · Subagents
Write a custom subagent
The built-in subagents (worker agents that run a delegated task in their own context and report back) get you a long way, but they’re general by design. explore is documented as read-only and fast, general can do anything - and none of them knows the one rule you keep having to enforce by hand. feedmill - the course’s illustrative Go feed aggregator - parses dozens of differently-shaped feeds, and every parser adapter has to normalize its source’s date into UTC before it hands a timestamp upward; a parser that quietly stores the feed’s local time is a bug that mis-sorts the reading queue while looking like a data problem. What you want is a worker whose entire job is to walk every adapter and check that rule - and which cannot edit a file even if it wanted to, because an auditor that can quietly “fix” what it finds is no longer an auditor. That’s a custom subagent, and OpenCode lets you define one in a single markdown file.
Two ways to make one
Section titled “Two ways to make one”There’s a command for it and there’s the file it writes. The command is the gentle on-ramp:
$ opencode agent createIt runs interactively - asks where to save it (global or project) and what it’s for, has you pick a starting permission posture, then generates the identifier and the system prompt and writes the markdown file for you (the command and its prompts are documented as of October 2026; agents documentation). That’s the right move the first time, because it shows you the shape. But the file is the agent, and once you’ve seen it you’ll usually just write the file directly, the same way you’d hand-edit opencode.json rather than re-run a wizard.
The file lives in one of two places, and the choice is the familiar one between ship-it-with-the-project and keep-it-on-your-machine:
.opencode/agents/auditor.md- checked into thefeedmillrepo, so the auditor ships with the project and every clone gets it.~/.config/opencode/agents/auditor.md- on your machine only, available in every project you open.
For a rule that’s specific to feedmill’s codebase, the project directory is the right home - the agent travels with the code it audits. The filename becomes the agent’s name: auditor.md gives you @auditor. (The plural agents/ is the canonical directory; the singular agent/ still loads too, as a backwards-compat alias - config documentation, as of October 2026.) If you wrote the skill-gated auditor on the skills gating page, this file replaces it at the same path; carry over its skill gate (the permission rule that limits which skills it may load) if you want to keep it.
The file: frontmatter is the contract
Section titled “The file: frontmatter is the contract”An OpenCode agent is YAML frontmatter plus a body. The frontmatter declares what the agent is and what it’s allowed to do; the body becomes its system prompt - the standing instructions it carries into every invocation. The fields used here are documented agent frontmatter as of October 2026 (agents documentation). Here’s the auditor, complete:
---description: > Read-only auditor. Walks every feed adapter and verifies each one normalizes its source timestamp to UTC before returning it. Reports violations; never fixes them.mode: subagentmodel: anthropic/claude-sonnet-4-6temperature: 0.1permission: edit: deny bash: "*": deny "git log*": allow "git show*": allow "git blame*": allow "*--output*": deny "*>*": deny task: deny webfetch: deny---
You audit feedmill's feed adapters for one rule and one rule only: everyadapter must return its source's date as a time.Time in UTC (for example,parse it and call `.UTC()`) before it returns a timestamp.
For each parser adapter under internal/feed/ and internal/adapter/, report:- the file and the line where the timestamp is produced- whether it normalizes to UTC, and if not, exactly what it does instead
Do not edit, suggest patches, or run anything that writes. List findingsas a table and stop. Finding zero violations is a valid, good result.Read that frontmatter field by field, because every line is doing a specific job:
descriptionis not decoration - it’s how a primary agent decides whether to delegate to this worker automatically (as of October 2026; agents documentation). Write it as a precise statement of the job, including the never fixes them clause, so neither you nor a primary agent ever reaches for this worker expecting it to change code.mode: subagentis the line that makes this a worker rather than a top-level driver. The three values areprimary(a top-level agent you Tab to, likebuildandplan),subagent(a worker you@mentionor that gets delegated to, never driven directly), andall(eligible for both). An auditor is something you call into a session, sosubagentis correct.modelpins this agent to its own model regardless of what your main session is running. The value is aprovider/model-idstring;anthropic/claude-sonnet-4-6is an illustrative ID from the catalog as of September 2026, and the IDs you can use depend on your connected providers. It’s the same per-agent override OpenCode supports for any configured agent, built-ins included (the per-agent model page works that tradeoff in depth). Audit work is read-and-judge, so a solid mid-tier reasoner is the right fit; you don’t need your most expensive model to check a rule, but you do need one that won’t miss a sneakytime.Parsewithout a location.temperature: 0.1keeps it boring on purpose. An auditor that gets creative is an auditor inventing violations.permissionis the part that makes this safe, and it’s worth its own beat.
The permission map is the whole point
Section titled “The permission map is the whole point”The promise of this agent is it can never edit. OpenCode lets you make that structurally true rather than a hope, through the per-tool permission map. Each key names a tool class and takes allow, deny, or ask:
permission: edit: deny bash: "*": deny "git log*": allow "git show*": allow "git blame*": allow "*--output*": deny "*>*": deny task: deny webfetch: denyedit: deny is the load-bearing line. It revokes the write/edit/patch tools entirely for this agent - there is no approval prompt to fat-finger past, because the capability isn’t there. That’s a real difference from an ask posture: ask means the agent can do it once you say yes; deny means the agent cannot do it at all. For an auditor you want the second, harder guarantee.
The bash key shows OpenCode’s command-pattern handling. Rather than a flat allow/deny, you give it a map of command patterns: the three git patterns let the auditor read history for context, while the catch-all "*": deny blocks every other shell command - so it can’t sed -i its way around the edit ban or run a build that writes generated files. Name the read subcommands one by one. * matches any characters, so a broad "git *" would also allow git checkout -- ., git reset --hard, and git commit, and each of those changes your working tree or your history. Every allowed pattern is a promise about every command it matches. Even git log and git show accept --output=<file>, which writes a file, so a later rule denies any command that carries that flag. A shell redirect is the other write path: OpenCode checks each command with its redirect attached, so git log > notes.txt matches "git log*" and would write the file (as of October 2026; OpenCode source, shell.ts). The last rule, "*>*", denies any command that contains >, which also blocks harmless redirects such as 2>&1. Matching is last-match-wins - the last matching rule wins, per the permissions documentation as of October 2026 - so the broad "*" rule comes first, the specific git exceptions come after it, and the --output and redirect denials come last so that they override them; widen or tighten by reordering.
task: deny stops the auditor from launching another subagent, such as general, whose own permission map is wider. Current OpenCode source already denies task to a subagent unless its map grants it (as of October 2026; OpenCode source, subagent-permissions.ts), but the docs don’t state that default, so the explicit line keeps the boundary in the file you read. (webfetch: deny just keeps the auditor’s nose in the local codebase - it has no reason to hit the network to check a date-handling rule.)
The result is a worker that is read-only by construction, not by good behavior: the edit tools, every shell command outside the three git reads (and any command with --output or a > redirect), subagent launches, and web fetches are all denied. Within those rules, you can hand it the codebase with no supervision because the worst it can do is report. That is the control principle: the boundary lives in the permission map, not in the prompt.
Run it
Section titled “Run it”With the file saved, the new worker is there to @mention like any built-in. No reload step is documented as of October 2026 - if a fresh mention doesn’t resolve, restarting OpenCode is the fallback to try. An illustrative run - the verified behavior is the read-only audit and the table it returns, not this exact wording. Assume feedmill has six adapters: internal/adapter/normalise.go (the Atom path), plus json.go, rss.go, rdf.go, reddit.go, and youtube.go under internal/feed/. Map them onto the parsers in your own repo:
> @auditor check every feed adapter for the UTC normalization rule
@auditor (subagent · anthropic/claude-sonnet-4-6) • Read internal/feed/, internal/adapter/ (8 files), internal/parse/date.go
| adapter | timestamp site | normalizes to UTC? | |--------------------|----------------------------|--------------------------| | normalise.go | normalise.go:23 Published: entry.Updated | NO - copies the raw string, never parsed | | rss.go | rss.go:71 time.Parse RFC1123Z, then .UTC() | yes | | json.go | json.go:16 DatePublished.UTC() | yes | | reddit.go | reddit.go:46 time.Unix(ts, 0) | NO - returns server-local time, not .UTC() |
… excerpt: 4 of 6 adapter rows shown
2 of the 4 shown normalize to UTC. normalise.go copies the Atom updated string through unparsed at line 23, and reddit.go produces a local-zone time.Time at line 46 and never converts it. Those are the rule violations. I have not modified anything.There’s the worker doing exactly its job and nothing else: it spawned in its own isolated context, read the adapter tree without polluting your main session, found the adapters that skip the UTC conversion, and stopped. It didn’t offer a patch: its prompt says not to, and its permissions leave it no way to apply one - and that constraint is why you can trust that the report changed nothing. Keeping the finder and the fixer separate is a design choice: the audit stays a report you review, and every change goes through a step where you decide.
Now you have the bugs pinned to normalise.go:23 and reddit.go:46, found by a worker you can re-run on every PR without a second thought. But one auditor walking every adapter file in sequence is still one worker doing serial work. Next, turn that into parallelism: Fan out across feed adapters.