Description
Summary
When an agent .md file in ~/.config/opencode/agents/ (or .opencode/agents/) declares prompt: in YAML frontmatter and has an empty markdown body, OpenCode silently overwrites the frontmatter prompt: value with the empty body, producing an agent whose prompt is "". At session-construction time, the empty agent prompt falls through to the default opencode build prompt with no warning, no error, no log message.
This is a silent-failure footgun adjacent to issue #24181 (which covers invalid-YAML frontmatter being silently dropped). Both stem from the same loader flow in config/agent.ts.
Impact
For our use case (specialised role agents with strict prompt fidelity requirements), this caused agents that were configured to use a specific custom system prompt to actually run with the default opencode build prompt instead. Symptoms included:
- Agent identifying as "opencode (oc)" rather than as the configured persona
- Agent paraphrasing rather than quoting source-prompt text verbatim
- Agent's behavioural constraints (forbidden phrases, formatting contracts) silently absent
These symptoms presented as "primary mode dilutes the agent prompt" and consumed ~36 hours across two investigations (one autonomous, one wire-level via MITM proxy) before the silent-override behaviour was identified at the source level.
Root cause
packages/opencode/src/config/agent.ts:135 (line numbers as of 1.14.41):
const config = {
name,
...md.data, // ← spreads YAML frontmatter (prompt: "..." here)
prompt: md.content.trim(), // ← body of the file ALWAYS overrides frontmatter prompt
}
Because prompt: md.content.trim() is applied AFTER ...md.data, any prompt: value in the YAML frontmatter is unconditionally overwritten — even when the markdown body is empty. There is no warning when:
(a) Both prompt: (in frontmatter) AND a non-empty body are present (ambiguous — which is intended?)
(b) prompt: is present in frontmatter but body is empty (likely user error)
(c) Both are empty (no prompt at all — definitely user error)
All three cases silently produce a possibly-misconfigured agent that the runtime then tries to use.
Steps to reproduce
-
Create ~/.config/opencode/agents/test-agent.md:
---
description: Test agent
mode: primary
model: <some-model>
prompt: "{file:/path/to/your/system-prompt.md}"
---
(Note: prompt: field present, body empty.)
-
Run opencode run --agent test-agent --dangerously-skip-permissions "Identify yourself."
-
Observed: agent responds as default opencode build agent ("I am opencode, an interactive CLI tool...").
-
Expected: agent responds using the system prompt at the {file:...} reference, OR opencode emits a clear error explaining that prompt: in frontmatter is ignored and the body is what's used.
Wire-level confirmation
We MITM-proxied the request from opencode run to LiteLLM and inspected the captured payload directly. The system message contained:
- The default opencode build prompt (~5 KB)
- AGENTS.md content (per
instructions: field)
- SHARED.md content
- Environment block
It did not contain any content from the file referenced in the agent's prompt: "{file:...}" field. Capture available on request.
Why this isn't obvious to the user
- The YAML frontmatter
prompt: field IS valid per the published AgentSchema (config/agent.ts:31: prompt: Schema.optional(Schema.String)). So users naturally try it.
- The
{file:...} substitution syntax DOES work in opencode.json/.jsonc for agent.<name>.prompt, leading users to (reasonably) expect it works in agent .md frontmatter too. It doesn't — ConfigVariable.substitute is only called inside loadConfig (config/config.ts:395), not on agent .md content.
- There's no log message, no schema-validation error, no agent-list warning. The agent appears in
opencode agent list, accepts invocations, and produces output. The output just isn't using the prompt the user thought they configured.
Suggested fixes (any of these would have caught our case at config-load time)
Option A — error on ambiguous/empty configs:
In config/agent.ts:load(), after parsing:
if (md.data.prompt && md.content.trim()) {
throw new InvalidError({ path: item, issues: [{ message:
"Agent has both YAML 'prompt:' AND non-empty body. The body always wins; 'prompt:' is silently ignored. Choose one." }] })
}
if (!md.data.prompt && !md.content.trim()) {
throw new InvalidError({ path: item, issues: [{ message:
"Agent has no prompt: empty frontmatter 'prompt:' and empty body." }] })
}
Option B — honour frontmatter prompt: if body is empty:
const config = {
name,
...md.data,
prompt: md.content.trim() || md.data.prompt || "",
}
This makes both forms work, with body taking precedence only when present. Backwards-compatible with existing agents that use the body convention.
Option C — at minimum, run ConfigVariable.substitute on agent .md content:
If the body of the agent .md is {file:/path/to/prompt.md}, that should resolve to the file's contents (consistent with the opencode.json behaviour). Currently it stays as a literal string. Independent of A/B but useful.
Option D — runtime warning if agent's resolved prompt is empty:
In session/llm.ts where the system messages are assembled, log a warning if agent.prompt is empty/whitespace before falling through to SystemPrompt.provider(model). This would catch any silent-empty-prompt path, not just this specific one.
We're happy to PR Option A + D if there's interest.
Workaround
Define agents inline in opencode.json/.jsonc agent: block instead of as standalone .md files. That layer DOES run ConfigVariable.substitute, so {file:...} works there. We've migrated all our agents to this pattern.
OpenCode version
1.14.41
Operating System
macOS 26 (darwin)
Related
Description
Summary
When an agent
.mdfile in~/.config/opencode/agents/(or.opencode/agents/) declaresprompt:in YAML frontmatter and has an empty markdown body, OpenCode silently overwrites the frontmatterprompt:value with the empty body, producing an agent whose prompt is"". At session-construction time, the empty agent prompt falls through to the default opencode build prompt with no warning, no error, no log message.This is a silent-failure footgun adjacent to issue #24181 (which covers invalid-YAML frontmatter being silently dropped). Both stem from the same loader flow in
config/agent.ts.Impact
For our use case (specialised role agents with strict prompt fidelity requirements), this caused agents that were configured to use a specific custom system prompt to actually run with the default opencode build prompt instead. Symptoms included:
These symptoms presented as "primary mode dilutes the agent prompt" and consumed ~36 hours across two investigations (one autonomous, one wire-level via MITM proxy) before the silent-override behaviour was identified at the source level.
Root cause
packages/opencode/src/config/agent.ts:135(line numbers as of 1.14.41):Because
prompt: md.content.trim()is applied AFTER...md.data, anyprompt:value in the YAML frontmatter is unconditionally overwritten — even when the markdown body is empty. There is no warning when:(a) Both
prompt:(in frontmatter) AND a non-empty body are present (ambiguous — which is intended?)(b)
prompt:is present in frontmatter but body is empty (likely user error)(c) Both are empty (no prompt at all — definitely user error)
All three cases silently produce a possibly-misconfigured agent that the runtime then tries to use.
Steps to reproduce
Create
~/.config/opencode/agents/test-agent.md:(Note:
prompt:field present, body empty.)Run
opencode run --agent test-agent --dangerously-skip-permissions "Identify yourself."Observed: agent responds as default opencode build agent ("I am opencode, an interactive CLI tool...").
Expected: agent responds using the system prompt at the
{file:...}reference, OR opencode emits a clear error explaining thatprompt:in frontmatter is ignored and the body is what's used.Wire-level confirmation
We MITM-proxied the request from
opencode runto LiteLLM and inspected the captured payload directly. The system message contained:instructions:field)It did not contain any content from the file referenced in the agent's
prompt: "{file:...}"field. Capture available on request.Why this isn't obvious to the user
prompt:field IS valid per the publishedAgentSchema(config/agent.ts:31:prompt: Schema.optional(Schema.String)). So users naturally try it.{file:...}substitution syntax DOES work inopencode.json/.jsoncforagent.<name>.prompt, leading users to (reasonably) expect it works in agent.mdfrontmatter too. It doesn't —ConfigVariable.substituteis only called insideloadConfig(config/config.ts:395), not on agent.mdcontent.opencode agent list, accepts invocations, and produces output. The output just isn't using the prompt the user thought they configured.Suggested fixes (any of these would have caught our case at config-load time)
Option A — error on ambiguous/empty configs:
In
config/agent.ts:load(), after parsing:Option B — honour frontmatter
prompt:if body is empty:This makes both forms work, with body taking precedence only when present. Backwards-compatible with existing agents that use the body convention.
Option C — at minimum, run
ConfigVariable.substituteon agent .md content:If the body of the agent .md is
{file:/path/to/prompt.md}, that should resolve to the file's contents (consistent with the opencode.json behaviour). Currently it stays as a literal string. Independent of A/B but useful.Option D — runtime warning if agent's resolved prompt is empty:
In
session/llm.tswhere the system messages are assembled, log a warning ifagent.promptis empty/whitespace before falling through toSystemPrompt.provider(model). This would catch any silent-empty-prompt path, not just this specific one.We're happy to PR Option A + D if there's interest.
Workaround
Define agents inline in
opencode.json/.jsoncagent:block instead of as standalone.mdfiles. That layer DOES runConfigVariable.substitute, so{file:...}works there. We've migrated all our agents to this pattern.OpenCode version
1.14.41
Operating System
macOS 26 (darwin)
Related
config/agent.ts:load()could fix both.