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.
Aider Implementation
Section titled “Aider Implementation”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:
aider/coders/base_prompts.py— BaseCoderPromptsclass with shared system_reminder, files_content, and repo_content templatesaider/coders/editblock_prompts.py— SEARCH/REPLACE block format instructionsaider/coders/wholefile_prompts.py— Whole file replacement instructionsaider/coders/udiff_prompts.py— Unified diff format instructionsaider/coders/architect_prompts.py— Planning-focused, delegates edits to editor modelaider/coders/ask_prompts.py— Read-only question answering modeaider/coders/chat_chunks.py—ChatChunksdataclass and cache control headers
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:
@dataclassclass 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 reminderThe all_messages() method concatenates them in a fixed order:
system → examples → readonly_files → repo → done → chat_files → cur → reminderNote 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.
format_chat_chunks() Flow
Section titled “format_chat_chunks() Flow”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: contentblocks - 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 endreminder == "user": appended to the final user message content
Dynamic Context Injection
Section titled “Dynamic Context Injection”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.
Cache Control Headers
Section titled “Cache Control Headers”add_cache_control_headers() (chat_chunks.py:28-41) places three ephemeral cache breakpoints:
- End of examples (or system if no examples)
- End of repo map (or readonly_files if no repo)
- 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.
Prompt Variants Per Coder
Section titled “Prompt Variants Per Coder”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}.
AGENTS.md Handling
Section titled “AGENTS.md Handling”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.
Codex Implementation
Section titled “Codex Implementation”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:
codex-rs/core/prompt.md— Main system prompt (~275 lines): identity, AGENTS.md spec, responsiveness guidelines, sandbox behavior, safety rulescodex-rs/core/src/models_manager/model_info.rs—BASE_INSTRUCTIONSconstant and personality template assemblycodex-rs/core/src/instructions/user_instructions.rs— AGENTS.md injection asUserInstructionswith XML wrapper
Base Instructions
Section titled “Base Instructions”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.
Model-Specific Instructions via API
Section titled “Model-Specific Instructions via API”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-99instructions_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.
Config Overrides
Section titled “Config Overrides”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;}Prompt Assembly
Section titled “Prompt Assembly”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.
AGENTS.md as User Messages
Section titled “AGENTS.md as User Messages”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.
Skill Instructions
Section titled “Skill Instructions”Skills (custom prompts) follow a similar pattern (lines 48-80):
<skill><name>{name}</name><path>{path}</path>{content}</skill>History Normalization
Section titled “History Normalization”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).
OpenCode Implementation
Section titled “OpenCode Implementation”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:
packages/opencode/src/session/prompt/anthropic.txt— Claude-family instructions (105 lines)packages/opencode/src/session/prompt/gemini.txt— Gemini instructions (155 lines)packages/opencode/src/session/prompt/codex_header.txt— GPT-5 / default fallback (79 lines)packages/opencode/src/session/prompt/trinity.txt— Trinity model instructions (97 lines)packages/opencode/src/session/prompt/qwen.txt— Qwen / fallback without TODO (109 lines)packages/opencode/src/session/system.ts— Model routing and environment context assemblypackages/opencode/src/session/instruction.ts— AGENTS.md discovery, claim tracking, dynamic resolution
Dual System Prompt Pattern
Section titled “Dual System Prompt Pattern”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.
Environment Context Block
Section titled “Environment Context Block”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.
Model-Specific Prompt Selection
Section titled “Model-Specific Prompt Selection”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 guidancegemini.txt— Gemini-specific guidancecodex_header.txt— Default fallback / GPT-5 instructionstrinity.txt— Custom model supportqwen.txt— Used asPROMPT_ANTHROPIC_WITHOUT_TODOfallback
The instructions() function (line 15-17) always returns the codex_header.txt content.
Instruction Assembly
Section titled “Instruction Assembly”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]Dynamic Instruction Resolution
Section titled “Dynamic Instruction Resolution”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.
Structured Output Injection
Section titled “Structured Output Injection”When the user requests JSON schema output (prompt.ts:655-657):
if (format.type === "json_schema") { system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)}Max Steps Injection
Section titled “Max Steps Injection”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,}] : []),Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”System vs. User Role for Instructions
Section titled “System vs. User Role for Instructions”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.
Model-Specific Prompts Are Necessary
Section titled “Model-Specific Prompts Are Necessary”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.
AGENTS.md Priority and Deduplication
Section titled “AGENTS.md Priority and Deduplication”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.
Token Budget vs. Instruction Quality
Section titled “Token Budget vs. Instruction Quality”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.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Crate: openoxide-prompt
Section titled “Crate: openoxide-prompt”System prompt construction should be a standalone crate that other components call.
Three-Layer Architecture
Section titled “Three-Layer Architecture”-
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. -
Provider-Specific Overrides: A
ProviderPrompttrait 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. -
Dynamic Context: Runtime-injected content:
- Environment block (working directory, platform, date, git status)
- AGENTS.md instructions (from
openoxide-instructionscrate, user-role messages) - Tool descriptions (auto-generated from tool registry)
Message Assembly Order
Section titled “Message Assembly Order”[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)Configuration
Section titled “Configuration”[prompt]base_instructions_file = "path/to/custom.md" # Override base promptprovider_prompt = "anthropic" # Force specific provider styleinclude_environment = true # Toggle env blockinclude_directory_tree = false # Disabled by defaultKey Design Decisions
Section titled “Key Design Decisions”- 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
Crates
Section titled “Crates”openoxide-prompt— Prompt assembly and provider routingopenoxide-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)