Skip to content

System Prompt

The system prompt is the foundation of every AI coding agent interaction. It defines the agent’s identity, capabilities, behavioral rules, and edit format instructions. Getting it right is a balancing act: too little context and the model halluccinates; too much and you burn tokens on preamble instead of actual work. Every reference implementation assembles the system prompt differently, reflecting fundamental architectural choices about where intelligence lives.

See also Skills for SKILL.md discovery, selection, and prompt injection mechanics that feed into system-prompt construction.

Commit: b9050e1d

Aider’s system prompt construction is tightly coupled to its edit format system. Each coder variant (EditBlock, WholeFile, Udiff, Architect, Ask) has its own prompt class that provides the base instructions. The ChatChunks dataclass orchestrates message assembly with explicit ordering.

Prompt source files:

ChatChunks: The Message Assembly Framework

Section titled “ChatChunks: The Message Assembly Framework”

All prompt content flows through ChatChunks (aider/coders/chat_chunks.py:5-27), a dataclass with eight ordered fields:

@dataclass
class ChatChunks:
system: List # Core system prompt (role or user/assistant pair)
examples: List # Few-shot examples of edit format
done: List # Completed conversation history (summaries)
repo: List # Repo map content
readonly_files: List # Read-only file content
chat_files: List # Files added to chat context
cur: List # Current turn messages
reminder: List # Edit format reminder

The all_messages() method concatenates them in a fixed order:

system → examples → readonly_files → repo → done → chat_files → cur → reminder

Note the ordering: readonly_files and repo come before done (conversation history). This ensures static context is stable for prompt caching, while the growing conversation tail lives at the end.

The core construction happens in base_coder.py:1226-1331:

Step 1 — Base Prompt (line 1228): Retrieved from the coder’s prompt class (e.g., EditBlockPrompts.main_system). The raw template includes placeholders like {fence} (for code fence characters) and {final_reminders}.

Step 2 — Model Prefix (lines 1229-1230): If the model config specifies a system_prompt_prefix, it gets prepended. This handles model-specific preambles.

Step 3 — Example Integration (lines 1232-1259): Few-shot examples can go two ways:

  • If model.examples_as_sys_msg == True: examples are appended directly into the system message as ## ROLE: content blocks
  • Otherwise: examples become separate user/assistant message pairs, followed by a “I switched to a new code base” reset message to prevent the model from referencing example file names

Step 4 — System Reminder Append (line 1261-1262): The system_reminder from the prompt class is appended to the main system message. This contains the edit format rules repeated for emphasis.

Step 5 — Role Assignment (lines 1266-1274): Models that support system role get a single system message. Models without system prompt support fall back to a user/assistant pair:

if self.main_model.use_system_prompt:
chunks.system = [dict(role="system", content=main_sys)]
else:
chunks.system = [
dict(role="user", content=main_sys),
dict(role="assistant", content="Ok."),
]

Step 6 — Conditional Reminder (lines 1315-1329): The reminder is only included if token budget allows. Two placement modes:

  • reminder == "sys": separate system message at the end
  • reminder == "user": appended to the final user message content

Repo map, read-only files, and chat files are each wrapped as user/assistant message pairs:

# Repo map (base_coder.py:750-761)
[dict(role="user", content=repo_content),
dict(role="assistant", content="Ok, I won't try to edit those files without asking first.")]
# Read-only files
[dict(role="user", content=readonly_prefix + content),
dict(role="assistant", content="Ok, I will use this...")]
# Chat files
[dict(role="user", content=files_prefix + content),
dict(role="assistant", content="Ok, any changes I propose...")]

The user/assistant pair pattern is deliberate — it teaches the model the expected acknowledgment behavior.

add_cache_control_headers() (chat_chunks.py:28-41) places three ephemeral cache breakpoints:

  1. End of examples (or system if no examples)
  2. End of repo map (or readonly_files if no repo)
  3. End of chat_files

These align with Anthropic’s prompt caching — stable prefixes get cached, while the growing conversation tail (done + cur + reminder) varies per turn.

Each coder class provides specialized prompts:

  • EditBlockPrompts: SEARCH/REPLACE block rules with examples
  • WholeFilePrompts: Full file replacement instructions
  • UdiffPrompts: Unified diff format instructions
  • ArchitectPrompts: Planning-focused, delegates edits to editor model
  • AskPrompts: Read-only, no edit instructions

The fmt_system_prompt() method (line 1197) interpolates variables like {fence}, {quad_fence}, {go_ahead_tip}, and {language}.

Aider does not auto-discover AGENTS.md files. Project instructions are injected only through explicit --read flags or the conventions mechanism. This is a deliberate simplification compared to Codex and OpenCode.


Commit: 4ab44e2c

Codex takes a server-driven approach: the base system prompt is fetched from the OpenAI models API as part of the ModelInfo response. Local overrides and AGENTS.md instructions are injected as user-role messages.

Prompt source files:

The fallback prompt lives at codex-rs/core/prompt.md and is embedded via include_str! in model_info.rs:15:

pub const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md");

This ~3KB prompt defines Codex’s identity (“You are a coding agent running in the Codex CLI”), capabilities, the AGENTS.md spec, responsiveness guidelines (preamble messages, plan updates), sandbox behavior, and safety rules. It is used only as a fallback when the models API doesn’t provide model-specific instructions.

In production, ModelInfo fetched from the models API contains a base_instructions field. The ModelsManager caches these with ETag-based revalidation (5-minute TTL). Each model slug can have different instructions optimized for its capabilities.

The model_messages field enables personality injection:

// model_info.rs:88-99
instructions_template: "{DEFAULT_PERSONALITY_HEADER}\n\n{{ personality }}\n\n{BASE_INSTRUCTIONS}"

The {{ personality }} placeholder gets replaced based on user preference (friendly vs. pragmatic) at get_model_instructions() time.

with_config_overrides() (model_info.rs:22-54) allows users to replace base instructions entirely via config.base_instructions. If set, model_messages (personality) is also cleared:

if let Some(base_instructions) = &config.base_instructions {
model.base_instructions = base_instructions.clone();
model.model_messages = None;
}

The Prompt struct (client_common.rs:27-45) carries the complete API request:

pub struct Prompt {
pub input: Vec<ResponseItem>, // Full conversation history
pub(crate) tools: Vec<ToolSpec>, // Available tools
pub(crate) parallel_tool_calls: bool,
pub base_instructions: BaseInstructions, // System prompt text
pub personality: Option<Personality>,
pub output_schema: Option<Value>,
}

In run_sampling_request() (codex.rs:4897-5023), the prompt is assembled:

let base_instructions = sess.get_base_instructions().await;
let prompt = Prompt {
input, // clone_history().for_prompt() — all conversation items
tools, // filtered for model capabilities
parallel_tool_calls: model_supports_parallel,
base_instructions, // from session_configuration.base_instructions
personality: turn_context.personality,
output_schema: turn_context.final_output_json_schema.clone(),
};

The base_instructions are sent as a separate API field (not as a user message). They correspond to the instructions parameter in the OpenAI Responses API — a dedicated system-level slot that sits outside the conversation history.

Project documentation is injected via UserInstructions (instructions/user_instructions.rs:7-45):

pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for ";
impl From<UserInstructions> for ResponseItem {
fn from(ui: UserInstructions) -> Self {
ResponseItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: format!(
"{USER_INSTRUCTIONS_PREFIX}{directory}\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>",
directory = ui.directory,
contents = ui.text
),
}],
}
}
}

These are user-role messages (not system), injected at the start of the conversation. The <INSTRUCTIONS> XML tags clearly delimit the content for both the model and debugging.

Skills (custom prompts) follow a similar pattern (lines 48-80):

<skill><name>{name}</name><path>{path}</path>{content}</skill>

Before sending, for_prompt() (context_manager/history.rs:82-91) strips ghost snapshots and normalizes for input modalities (removes images if the model doesn’t support them).


Commit: 7ed44997

OpenCode assembles the system prompt from two independent sources: an environment context block and model-specific agent instructions, joined at prompt time.

Prompt source files:

In session/prompt.ts:653:

const system = [
...(await SystemPrompt.environment(model)),
...(await InstructionPrompt.system())
]

This produces an array of strings, each becoming a separate system message. The LLM sees them as a sequence of system-level instructions.

session/system.ts:29-53 generates dynamic environment metadata:

export async function environment(model: Provider.Model) {
return [
[
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${Instance.directory}`,
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
`</env>`,
].join("\n"),
]
}

Notably, the directory tree listing is disabled (&& false guard on line 43). This was likely found to be too token-expensive in practice.

system.ts:19-27 routes to different prompt files based on model ID:

export function provider(model: Provider.Model) {
if (model.api.id.includes("-codex")) return [PROMPT_CODEX]
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY]
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}

Five embedded prompt files live in session/prompt/:

  • anthropic.txt — Claude-family instructions (“You are OpenCode, the best coding agent on the planet.”)
  • beast.txt — GPT-family with extended workflow guidance
  • gemini.txt — Gemini-specific guidance
  • codex_header.txt — Default fallback / GPT-5 instructions
  • trinity.txt — Custom model support
  • qwen.txt — Used as PROMPT_ANTHROPIC_WITHOUT_TODO fallback

The instructions() function (line 15-17) always returns the codex_header.txt content.

session/instruction.ts:118-145 provides the InstructionPrompt.system() function that loads AGENTS.md and config-specified instruction files:

Project-level discovery (lines 75-85): Searches for AGENTS.md, CLAUDE.md, or CONTEXT.md (deprecated) using Filesystem.findUp() from the working directory up to the worktree root. Stops at the first filename that matches — if AGENTS.md exists, CLAUDE.md is skipped.

Global files (lines 19-29): Checks $OPENCODE_CONFIG_DIR/AGENTS.md, then ~/.opencode/AGENTS.md, then ~/.claude/CLAUDE.md (unless OPENCODE_DISABLE_CLAUDE_CODE_PROMPT is set). Again, first match wins.

Config-specified instructions (lines 94-113): The config.instructions array supports both file paths (with ~/ expansion and glob patterns) and HTTP(S) URLs. File paths can be relative (resolved via Filesystem.globUp() from CWD to worktree root) or absolute (resolved via Bun.Glob).

Remote fetching (lines 137-142): URLs are fetched with a 5-second timeout via AbortSignal.timeout(5000). Failures are silently ignored.

Each instruction source is prefixed with attribution:

Instructions from: /path/to/AGENTS.md
[content]

Beyond system-level instructions, OpenCode has a resolve() function (instruction.ts:171-196) that discovers AGENTS.md files per file access. When the AI reads or edits a file in a subdirectory, resolve() walks from that directory up to the project root, checking for undiscovered instruction files. It uses a claim tracking system (lines 44-69) to prevent duplicate injection:

const state = Instance.state(() => ({
claims: new Map<string, Set<string>>(),
}))

Claims are scoped per messageID, so different turns can independently discover the same instruction file.

When the user requests JSON schema output (prompt.ts:655-657):

if (format.type === "json_schema") {
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
}

When the agent reaches its step limit, a synthetic assistant message is injected (prompt.ts:667-674):

...(isLastStep ? [{
role: "assistant" as const,
content: MAX_STEPS,
}] : []),

Aider and OpenCode both support models without native system prompt support. Aider falls back to user/assistant pairs, which works but wastes a turn of context. Codex avoids this entirely by using the Responses API instructions field, which is API-level separation.

Prompt Caching Depends on Prefix Stability

Section titled “Prompt Caching Depends on Prefix Stability”

Aider’s cache breakpoint placement (after examples, after repo, after chat_files) only works if those sections are stable across turns. Any change to the repo map or file contents invalidates the cache. Codex relies on server-side caching keyed by conversation ID, avoiding this problem entirely.

OpenCode’s five-way model routing (system.ts:19-27) reflects a hard-learned lesson: different model families respond better to different instruction styles. Claude prefers structured XML-tagged sections, GPT models work better with natural language, and Gemini needs its own conventions. A one-size-fits-all system prompt degrades performance across providers.

Codex’s <INSTRUCTIONS> XML wrapping with directory attribution is cleanest for debugging. OpenCode’s claim tracking system prevents duplicate injection but adds complexity. Aider sidesteps the problem by not auto-discovering instruction files.

The system prompt is the most expensive fixed cost in every turn. Codex’s approach of fetching optimized instructions from the API per model slug is most efficient — the instructions are tuned by the provider. OpenCode’s embedded prompt files (~2-5KB each) are reasonable. Aider’s approach of embedding edit format examples can be token-heavy but necessary for format compliance.

Environment Context Is Low-Value Per Token

Section titled “Environment Context Is Low-Value Per Token”

OpenCode disabled directory tree listing in the environment block (&& false guard). The working directory, platform, and date are useful; a full directory listing is not worth the tokens.


System prompt construction should be a standalone crate that other components call.

  1. Base Instructions: Embedded Markdown file (include_str!), ~2KB. Defines OpenOxide identity, AGENTS.md spec, safety rules, and general behavior guidelines. Serves as the fallback when provider-specific instructions are unavailable.

  2. Provider-Specific Overrides: A ProviderPrompt trait with implementations per provider family:

    pub trait ProviderPrompt: Send + Sync {
    fn system_instructions(&self) -> &str;
    fn supports_system_role(&self) -> bool;
    }

    Implementations: AnthropicPrompt, OpenAiPrompt, GeminiPrompt, LocalPrompt.

  3. Dynamic Context: Runtime-injected content:

    • Environment block (working directory, platform, date, git status)
    • AGENTS.md instructions (from openoxide-instructions crate, user-role messages)
    • Tool descriptions (auto-generated from tool registry)
[system] base_instructions + provider_override
[system] environment_context
[user] AGENTS.md instructions (with <INSTRUCTIONS> tags, Codex-style)
[user/assistant] conversation history
[system] edit format reminder (conditional on token budget)
[prompt]
base_instructions_file = "path/to/custom.md" # Override base prompt
provider_prompt = "anthropic" # Force specific provider style
include_environment = true # Toggle env block
include_directory_tree = false # Disabled by default
  • Codex-style <INSTRUCTIONS> tags for AGENTS.md injection — parseable and debuggable
  • Provider routing based on model ID prefix matching (OpenCode pattern) — different models need different prompts
  • Conditional reminder based on token budget (Aider pattern) — prevents token overflow
  • No embedded examples in system prompt — too expensive; rely on model training instead
  • Cache breakpoint hints in the message array for Anthropic’s prompt caching — place after static prefix, before conversation history
  • openoxide-prompt — Prompt assembly and provider routing
  • openoxide-instructions — AGENTS.md discovery and loading (already designed in agents-md.md)
  • openoxide-provider — Provider trait and model metadata (already designed in provider-abstraction.md)