Skip to content

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.

There’s a command for it and there’s the file it writes. The command is the gentle on-ramp:

Terminal window
$ opencode agent create

It 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 the feedmill repo, 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.

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: subagent
model: anthropic/claude-sonnet-4-6
temperature: 0.1
permission:
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: every
adapter 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 findings
as 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:

  • description is 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: subagent is the line that makes this a worker rather than a top-level driver. The three values are primary (a top-level agent you Tab to, like build and plan), subagent (a worker you @mention or that gets delegated to, never driven directly), and all (eligible for both). An auditor is something you call into a session, so subagent is correct.
  • model pins this agent to its own model regardless of what your main session is running. The value is a provider/model-id string; anthropic/claude-sonnet-4-6 is 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 sneaky time.Parse without a location.
  • temperature: 0.1 keeps it boring on purpose. An auditor that gets creative is an auditor inventing violations.
  • permission is the part that makes this safe, and it’s worth its own beat.

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: deny

edit: 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.

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.