Skip to content

Bug: agent .md prompt: frontmatter field is silently overridden by markdown body, producing empty prompt and silent fallback to default agent prompt #26434

@CyberFreedomOrg

Description

@CyberFreedomOrg

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

  1. 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.)

  2. Run opencode run --agent test-agent --dangerously-skip-permissions "Identify yourself."

  3. Observed: agent responds as default opencode build agent ("I am opencode, an interactive CLI tool...").

  4. 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

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions