opencode¶
opencode is pure path-discovery — it reads instructions and skills from fixed locations and needs no flags — so in wrapper mode every scope is a bind and nothing is appended to your command. It is the closest harness to Claude; the one difference is that the project instructions are read in place rather than relocated.
Wrapper resolution¶
| Source | Injected at |
|---|---|
project AGENTS.md |
(not injected — read natively at ./AGENTS.md) |
project .agents/skills/ |
<root>/.opencode/skills/ |
global ~/.config/agents/AGENTS.md (+ optional AGENTS.opencode.md overlay) |
$XDG_CONFIG_HOME/opencode/AGENTS.md (default ~/.config/opencode/AGENTS.md) |
global ~/.config/agents/skills/ |
$XDG_CONFIG_HOME/opencode/skills/ (default ~/.config/opencode/skills/) |
- Project instructions — opencode reads the root
AGENTS.md(traversing up from the work dir) as its project rules file. That is exactly the agent-neutral source, already in place, so agedum injects nothing for it — and never could, since the rootAGENTS.mdis git-tracked. - Global instructions — opencode reads
~/.config/opencode/AGENTS.mdas its user-scope rules file, so the globalAGENTS.mdis bound there — base merged with an optionalAGENTS.opencode.mdoverlay. - Skills — compiled with the
SKILL.opencode.mdoverlay and bound to./.opencode/skills/(project) and~/.config/opencode/skills/(global). opencode searches those directories before the project's raw.agents/skills/(which it would otherwise read directly), so the overlaid copy wins. The global skills source is~/.config/agents/skills/, delivered only via the bind above. extra_args: none — opencode discovers everything from disk, like Claude.
agedum --wrapper opencode -- opencode run "review this change"
agedum --wrapper opencode -- opencode # interactive TUI
Provider config¶
opencode resolves provider credentials from its own auth store (opencode auth
login), so a key is only in requiredEnv when opencode itself reads it from the
environment — or when a providerDef bakes it into the config. The
config block is translated into opencode's OPENCODE_CONFIG_CONTENT document (a single
env var; no file written):
{
"harness": "opencode",
"slug": "opencode-deepseek",
"requiredEnv": ["DEEPSEEK_API_KEY"],
"config": {
"model": "deepseek/deepseek-v4-pro",
"disableExternalSkills": true,
"effortLevel": "high",
"agentOptions": [
{ "agent": "general", "model": "deepseek/deepseek-v4-flash", "reasoningEffort": "low" }
]
}
}
config key |
Effect |
|---|---|
model |
model field of OPENCODE_CONFIG_CONTENT |
disableExternalSkills |
OPENCODE_DISABLE_EXTERNAL_SKILLS=1 |
defaultOptions.{reasoningEffort,textVerbosity,reasoningSummary} |
the default model's provider.<id>.models.<model>.options |
effortLevel (flat alias) |
the default model's reasoningEffort (explicit defaultOptions.reasoningEffort wins) |
agentOptions[] |
per-agent agent.<name> model + options; primary: true sets mode: "primary" for custom (non-built-in) agents |
providerDef |
an explicit provider block with the key resolved from the environment — see below |
opencodeConfig |
a literal opencode config object, deep-merged last (wins on conflict) — see below |
opencodeConfig.agent.<name>.agentAppend |
per-agent instructions folded onto the end of that agent's prompt — see below |
emitTranscript |
inject the bundled transcript-capture plugin (default on); set false to opt out — see below |
providerDef — declare the provider + key inline¶
By default an opencode model like openrouter/deepseek/deepseek-v4-pro relies on
opencode resolving the openrouter provider from its own auth store. providerDef
instead defines the provider in the config and resolves the API key from the
environment, so no prior login is needed:
{
"harness": "opencode",
"requiredEnv": ["OPENROUTER_API_KEY"],
"config": {
"model": "openrouter/deepseek/deepseek-v4-pro",
"providerDef": {
"id": "openrouter",
"npm": "@openrouter/ai-sdk-provider",
"baseUrl": "https://openrouter.ai/api/v1",
"apiKeyEnv": "OPENROUTER_API_KEY"
}
}
}
| Field | Meaning |
|---|---|
id |
provider id; must match the prefix of the model strings (e.g. openrouter) |
npm |
the AI-SDK package opencode loads for the provider |
baseUrl |
becomes provider.<id>.options.baseURL |
apiKeyEnv |
env var whose value is resolved into provider.<id>.options.apiKey |
The key's value (not a {env:…} placeholder) is written into
provider.<id>.options.apiKey, because opencode's {env:…} substitution is unreliable for
a custom provider's options.apiKey. This is the same in-process token handling claude
uses for ANTHROPIC_AUTH_TOKEN; apiKeyEnv is auto-added to the validated requiredEnv,
and the resulting OPENCODE_CONFIG_CONTENT is masked in --dry-run. (Keys containing "
or \ would break the surrounding JSON; standard sk-or-… keys are fine.)
providerDef may also be a list when one config draws models from more than one
provider — e.g. a Kimi primary model plus DeepSeek fast subagents, each needing its own
baked-in key. Entries apply in order (later deep-merge over earlier), and every entry's
apiKeyEnv is auto-added to requiredEnv:
{
"harness": "opencode",
"requiredEnv": ["KIMI_API_KEY", "DEEPSEEK_API_KEY"],
"config": {
"model": "kimi-for-coding/kimi-k2.6",
"agentOptions": [
{ "agent": "general", "model": "deepseek/deepseek-v4-flash" }
],
"providerDef": [
{ "id": "kimi-for-coding", "npm": "@ai-sdk/anthropic", "baseUrl": "https://api.kimi.com/coding/v1", "apiKeyEnv": "KIMI_API_KEY" },
{ "id": "deepseek", "npm": "@ai-sdk/openai-compatible", "baseUrl": "https://api.deepseek.com", "apiKeyEnv": "DEEPSEEK_API_KEY" }
]
}
}
emitTranscript — in-band transcript capture (default on)¶
opencode runs as a full-screen alternate-screen TUI, so a terminal capturer (condash,
script, tmux, asciinema) only ever sees the current frame — the conversation that scrolls
inside the TUI is repainted, never retained. agedum ships and auto-injects a small
opencode plugin (agedum/assets/opencode/transcript-osc.js) that streams each finalized
message into the terminal as a neutral OSC escape the terminal ignores for display:
A capturer recovers a clean transcript by reassembling the base64 pieces and decoding the
JSON frames ({v,t:"msg",sid,mid,role,text} / {v,t:"end"}, where role is user,
assistant, or reasoning). The protocol names no viewer, so agedum stays
viewer-agnostic. The same frames are also appended as newline-delimited JSON to the
per-tab sidecar file named by $CONDASH_TRANSCRIPT_FILE when a capturer (condash) sets
it — a reliable transport for a capturer that reads a file rather than the pty's /dev/tty
echo, which a TUI's controlling terminal can hide. The plugin path is appended to
OPENCODE_CONFIG_CONTENT.plugin (unioned
with any opencodeConfig.plugin); agedum's bwrap launch binds the whole filesystem, so the
bundled path resolves inside the namespace. Set "emitTranscript": false to disable.
opencodeConfig — anything agedum doesn't model¶
The keys above are the common, cross-harness-meaningful knobs. For any other opencode
setting, drop it into opencodeConfig in opencode's own config shape — it is
deep-merged into the generated document last, so it overrides the modeled keys on conflict:
{
"harness": "opencode",
"config": {
"model": "deepseek/deepseek-v4-pro",
"effortLevel": "high",
"opencodeConfig": {
"theme": "tokyonight",
"agent": { "build": { "temperature": 0.2 } }
}
}
}
opencodeConfig must be a JSON object (a non-object is an error). It is the one escape
hatch you need for opencode: the modeled keys cover the common cases tersely and stay
consistent with the other harnesses, and anything else is written in opencode's own format
here.
agentAppend — per-agent instruction append¶
An agent's narrative prompt describes its role. Some rules are neither role description
nor permission — e.g. a workflow trigger like "if asked to change a sibling repo, hand
off to the build agent". agentAppend lets those live beside the prompt instead of
inside it: declare it in the agent's opencodeConfig.agent.<name> block, next to its
prompt, and agedum folds it onto the end of that agent's prompt — a single blank
line between — before the config reaches opencode. The synthetic agentAppend key is
stripped, so opencode only ever sees one prompt. (It lives in the opencodeConfig
passthrough beside prompt, not in agentOptions — like prompt, which is also a
passthrough-only field.)
{
"harness": "opencode",
"config": {
"opencodeConfig": {
"agent": {
"conception": {
"mode": "primary",
"prompt": "You are the planning agent. Plan first, then act.",
"agentAppend": "## Handoff rule\n\nIf asked to edit a sibling repo, hand off to the build agent — do not edit it yourself."
}
}
}
}
}
opencode then receives, for the conception agent:
You are the planning agent. Plan first, then act.
## Handoff rule
If asked to edit a sibling repo, hand off to the build agent — do not edit it yourself.
- String or list. A string is appended after trimming its surrounding whitespace; a list of strings is trimmed per entry and joined with a blank line between entries (so each block keeps its own heading) — use it to stack several independent rules. The prompt and the append are always separated by exactly one blank line (surrounding whitespace is not preserved).
- Heading is yours. agedum adds no heading of its own; write the
## …(or none) inside theagentAppendtext so you control the rendering. - Inheritance. Because it is an ordinary config field,
agentAppendflows throughextends: a base can define it for an agent and a child inherits it. A child overrides it by setting its own value, or clears an inherited append by setting it tonull. An agent withagentAppendbut nopromptgets the append text as its whole prompt; an agent whosepromptis not a string is an error. - Per-agent, opencode-only. It attaches to one named agent, so it is meaningful only for
opencode — the sole harness that carries per-agent prompts in the provider config. The
other harnesses draw their instructions from
AGENTS.md(with the per-harnessAGENTS.<harness>.mdoverlay), which is where a shared, non-agent-specific rule belongs.