System Prompt Ideas for Keen Code¶
Synthesised from a deep exploration of opencode and kimi-cli, cross-referenced with keen-code's current architecture.
Background: What the Reference Codebases Do¶
opencode¶
- Per-model system prompt files — ships a different
.txtfile for each model family:anthropic.txt,gemini.txt,beast.txt(GPT-family),qwen.txt(everything else),trinity.txt,codex_header.txt(codex/GPT-5). Routing happens insystem.ts. - Layered assembly — the final prompt is built from four ordered sources in
session/system.ts: - Model-specific base prompt
environment()block — working dir, platform, date, model name, git statusskills()block — dynamically discovered skill filesInstructionPrompt— project'sAGENTS.md/CLAUDE.mdfound by walking up the directory tree, plus global config files and even remote URLs- Separate modal prompts — plan mode gets
plan.txtinjected as a<system-reminder>tag; max-steps limit getsmax-steps.txtappended at the end. These are injected at runtime, not baked into the base prompt. - Tool descriptions are separate — each tool (
bash.txt,edit.txt,glob.txt, etc.) has its own.txtfile; kept completely apart from the system prompt. - Identity first — every prompt starts with
"You are OpenCode, the best coding agent on the planet."— a strong, confident identity declaration before anything else. - Key themes across all prompts:
- CLI-first tone (concise, no emojis unless asked, GitHub-flavoured markdown)
- Respect project conventions — never assume libraries, mimic existing style
- Professional objectivity — no sycophancy
- Task management via
TodoWrite - Parallel tool calls for efficiency
- Never commit unless asked; never create files unless necessary
- Safety rules (no malicious code, no secrets in commits)
file_path:line_numbercode references
kimi-cli¶
- Single base prompt (
system.md) with template interpolation — one markdown file is the base, with${ROLE_ADDITIONAL},${KIMI_NOW}(current datetime),${KIMI_WORK_DIR},${KIMI_WORK_DIR_LS}(live directory listing!),${KIMI_AGENTS_MD}(projectAGENTS.mdcontents inlined), and${KIMI_SKILLS}slots. - Working directory context injected live — injects the actual
lsoutput of the current working directory directly into the system prompt so the model immediately sees the project structure without a tool call. AGENTS.mdcontent inlined — rather than just telling the agent to readAGENTS.md, kimi-cli reads it and pastes it into the system prompt itself.- Skills as first-class citizens — available skills are described in the system prompt with
instructions to lazily read the
SKILL.mdfile only when needed. init.mdfor project bootstrapping — a separate prompt for the "explore this project and write anAGENTS.md" bootstrapping task.compact.mdfor context compression — a dedicated prompt for compacting conversation history with structured XML output format, to manage context window limits.- Language mirroring —
"You MUST use the SAME language as the user"— explicit multilingual behaviour baked into the prompt. - Separate coding vs. research vs. multimedia guidelines — distinct sections depending on the class of task.
- Minimal git mutation rule — never run
git commit/push/reset/rebasewithout explicit user confirmation.
keen-code (current state)¶
- No system prompt at all —
AppState.messagesstarts empty; messages are just accumulated user/assistant pairs. TheRoleSystemtype exists but is never used. - The infrastructure is ready —
toOpenAIMessagesalready handlesRoleSystem → openai.SystemMessage(m.Content), so prepending a system message is a one-line change. - Tools are richly implemented —
bash,read_file,write_file,edit_file,glob,grepare all present with permission gating.
Ideas¶
Idea 1 — Single Embedded System Prompt (simplest, ship it now)¶
Approach: Add one hardcoded system prompt string, injected as the very first message in every conversation. No dynamic content, no file I/O.
// internal/llm/systemprompt.go
const DefaultSystemPrompt = `You are Keen Code, an expert coding agent running in your terminal.
You help with software engineering tasks: fixing bugs, adding features, refactoring, explaining code.
# Tone and style
- Be concise and direct. Responses display on a CLI rendered in monospace. Use GitHub-flavored markdown.
- No emojis unless asked. No unnecessary preamble or postamble.
- One-word answers are fine for simple questions.
# Coding approach
- Explore before acting: use grep/glob to understand the codebase first.
- Follow existing conventions: mimic the style, naming, and patterns of the project.
- Never assume a library is available — check package.json, go.mod, etc. first.
- Make minimal changes. Prefer editing existing files over creating new ones.
- Never commit unless explicitly asked.
- Never add comments unless necessary.
# Tool usage
- Run independent tool calls in parallel for efficiency.
- Use read_file/write_file/edit_file for file ops; bash for shell commands.
- Reference code as ` + "`file_path:line_number`" + ` for easy navigation.
# Safety
- Never introduce code that logs, exposes, or commits secrets or API keys.
- Refuse requests to write malicious code.`
Injected in state.go's StreamChat:
func (s *AppState) StreamChat(ctx context.Context, cfg *config.ResolvedConfig) (<-chan StreamEvent, error) {
messages := s.GetMessages()
withSystem := append([]llm.Message{{Role: llm.RoleSystem, Content: llm.DefaultSystemPrompt}}, messages...)
return s.llmClient.StreamChat(ctx, withSystem, s.toolRegistry)
}
Pros: Instant improvement, zero complexity, works for all models.
Cons: No dynamic environment context; same prompt for all models and providers.
Idea 2 — Static Prompt + Dynamic Environment Block (recommended baseline)¶
Approach: Split the system prompt into a static identity/behaviour section and a dynamic
environment section assembled at call time — directly inspired by opencode's system.ts.
// internal/llm/systemprompt.go
func BuildSystemPrompt(workingDir string) string {
var sb strings.Builder
// 1. Static identity + behaviour
sb.WriteString(staticPrompt)
// 2. Dynamic environment block (like opencode's environment())
sb.WriteString(fmt.Sprintf(`
<env>
Working directory: %s
Platform: %s
Today's date: %s
Is git repo: %v
</env>`,
workingDir,
runtime.GOOS,
time.Now().Format("2006-01-02"),
isGitRepo(workingDir),
))
// 3. Directory listing (like kimi-cli's ${KIMI_WORK_DIR_LS})
if ls := quickDirListing(workingDir, 40); ls != "" {
sb.WriteString("\n\nTop-level directory structure:\n```\n" + ls + "\n```")
}
return sb.String()
}
Why the directory listing matters: The model immediately sees the project's top-level shape
(Go vs Node vs Python, monorepo vs single package) without spending a tool call on it. kimi-cli
injects a live ls output; opencode tried a full tree via ripgrep but found it too large — 40
top-level entries is the right middle ground.
Pros: Agent immediately knows where it is and the project shape without a tool call; date and
platform awareness improve command suggestions (e.g. open vs xdg-open).
Cons: Slightly larger prompt; directory listing could be stale if the user changes directory
mid-session (mitigated by regenerating on every StreamChat call).
Idea 3 — AGENTS.md Auto-Loading (high-value, low-cost)¶
Approach: Walk up the directory tree from workingDir looking for an AGENTS.md (or
CLAUDE.md), read it, and inline it into the system prompt — exactly what both opencode and
kimi-cli do.
func loadProjectInstructions(workingDir string) string {
candidates := []string{"AGENTS.md", "CLAUDE.md"}
for _, name := range candidates {
// Walk up from workingDir to find the file
content, path := findUpward(workingDir, name)
if content != "" {
return fmt.Sprintf("\n\n# Project Instructions (from %s)\n%s", path, content)
}
}
return ""
}
findUpward algorithm:
start: workingDir (e.g. /Users/alice/projects/my-api)
check: /Users/alice/projects/my-api/AGENTS.md → found → done
or
check: /Users/alice/projects/my-api/AGENTS.md → not found
check: /Users/alice/projects/AGENTS.md → not found
check: /Users/alice/AGENTS.md → found → done
stop at filesystem root
Why this matters: If a user drops an AGENTS.md in their repo describing the build commands,
coding style, and test strategy, the agent will immediately respect it without the user having to
repeat themselves every session. Walking upward means launching from a subdirectory still finds
the repo-root AGENTS.md.
Pros: Zero user friction — drop a file and it is automatically respected; works for any
project type.
Cons: Reading file from disk on every call (mitigated by the file being small and the OS page
cache making it effectively free on repeat calls).
Note: Ideas 2 and 3 are designed to be implemented together as a single cohesive unit. See
output-3_system-prompt.mdfor the full implementation plan covering both ideas.
Idea 4 — Per-Provider System Prompts (opencode-style routing)¶
Approach: Different models behave differently. The most important distinction for keen-code is between reasoning models (which have internal chain-of-thought) and standard models (which benefit from more verbose workflow guidance in the prompt).
// internal/llm/systemprompt.go
func GetSystemPromptForModel(model string) string {
switch {
case strings.Contains(model, "deepseek-reasoner") || strings.Contains(model, "-r1"):
return reasonerSystemPrompt // Shorter, action-focused
default:
return defaultSystemPrompt // Full workflow guidance
}
}
Model-specific prompt differences:
| Model Family | Key Difference | Prompt Adjustment |
|---|---|---|
DeepSeek R1 (deepseek-reasoner) |
Has extended internal thinking; reasons about plans automatically | Shorter prompt, action-focused, no "think before you act" instruction |
| DeepSeek V3 / Moonshot / kimi-k2 | No built-in thinking step | Full workflow guidance, explicit explore-first instruction |
| Future Claude support | Prefers XML tags, has strong instruction following | XML-structured prompt with <instructions> tags |
| Future GPT-4o support | Responds well to role-playing framing | Strong identity declaration up front |
For reasoning models like DeepSeek R1, the system prompt should be leaner — the model's chain-of-thought handles the planning step internally, so repeating "think before you act" in the prompt is redundant and wastes tokens.
Pros: Extracts the best behaviour from each model family; avoids wasting tokens on reasoning
models; easy to extend as new providers are added.
Cons: More maintenance surface; prompts can drift out of sync; requires observing concrete
model-specific failure modes before knowing what to tune.
Idea 5 — Structured Multi-Section Prompt with Clear Headers¶
Approach: Inspired by kimi-cli's clean markdown section structure. Rather than flowing prose
or a flat bullet list, organize the prompt into distinct named sections with # headers that the
model can reliably locate and reference during generation.
You are Keen Code, an expert coding agent for your terminal.
# Core Behaviour
[Identity, tone, conciseness rules — weights heavily because it is first]
# Coding Workflow
[Explore-first, minimal changes, follow conventions, verify with tests]
# Tool Usage Policy
[Which tools to use when, parallel calls, file_path:line_number references]
# Git Rules
[Never commit/push/reset/rebase without explicit confirmation]
# Security
[No secrets, no malicious code, refuse suspicious requests]
Why section ordering matters: LLMs weight early content more heavily than late content. The correct order is: identity → tone → workflow → tools → constraints. Putting safety rules first (as some prompts do) makes the model over-cautious. Putting them last makes them feel like afterthoughts. Constraints belong at the end — they apply to an agent that is already behaving correctly in all other respects.
Pros: Highly readable and maintainable; easy to add, remove, or reorder sections; the model
can reference specific sections by name.
Cons: Pure organisational choice — no functional difference from equivalent prose at inference
time.
Idea 6 — Plan Mode via <system-reminder> Injection¶
Approach: Following opencode's plan.txt pattern, add a /plan command to keen-code that
injects a <system-reminder> block into the next outgoing message, flipping the agent into
read-only analysis mode without modifying AppState.messages.
// When the user types /plan, prepend this to their next user message:
const planModeReminder = `<system-reminder>
PLAN MODE ACTIVE — READ ONLY. Do NOT edit, write, or run mutating bash commands.
Explore the codebase, analyse the problem, and describe a detailed implementation plan.
Ask clarifying questions if needed before proposing any solution. Do not execute anything.
</system-reminder>
`
// Usage in StreamChat:
if s.planMode {
userContent = planModeReminder + userContent
s.planMode = false // one-shot
}
Why <system-reminder> and not a new system message? Most LLM APIs only support one system
message. Injecting the reminder into the user turn as a clearly labelled XML block achieves the
same effect without breaking the message structure. opencode uses exactly this pattern.
Natural pairings:
- /plan → describe what you would do (read-only)
- /do → now actually do it (normal mode)
- ESC to interrupt either mode
Pros: High-value UX feature; prevents the agent from making changes when the user just wants
a proposal; pairs naturally with the existing ESC-to-interrupt flow.
Cons: Requires a new command in the REPL input handler; slightly more complex state to track
(even if just a single bool).
Idea 7 — AGENTS.md Self-Update Reminder¶
Approach: A single line added to the static prompt (or to the project instructions section)
instructing the agent to keep AGENTS.md current when it modifies structures mentioned in it.
If you modify any structures, configurations, commands, or workflows that are
described in AGENTS.md, you MUST update AGENTS.md to reflect those changes.
kimi-cli includes this exact pattern. The effect is a positive feedback loop: the agent that modifies the codebase also keeps the AI instructions for future sessions accurate.
Where to put it: Append to the project instructions section only when an AGENTS.md file
was actually found (no point in the instruction if the file doesn't exist):
if agentsMdPath != "" {
instructions += fmt.Sprintf(
"\n\nIf you modify anything described in this file, update %s to reflect the changes.",
agentsMdPath,
)
}
Pros: Free improvement — one sentence, high leverage; encourages a self-documenting project
over time.
Cons: The agent may over-eagerly edit AGENTS.md for minor changes; can be mitigated by
saying "significant structural changes" instead of "any changes".
Idea 8 — Global ~/.keen/AGENTS.md for User-Level Instructions¶
Approach: In addition to the project-level AGENTS.md walk, read a global instructions file
from ~/.keen/AGENTS.md (or ~/.config/keen/AGENTS.md) and prepend it before the project
instructions. opencode reads from ~/.config/opencode/instructions/ and even supports remote
URL instructions.
func globalInstructions() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
candidates := []string{
filepath.Join(home, ".keen", "AGENTS.md"),
filepath.Join(home, ".config", "keen", "AGENTS.md"),
}
for _, path := range candidates {
content, err := os.ReadFile(path)
if err == nil && len(content) > 0 {
return fmt.Sprintf("# Global User Instructions (from %s)\n%s", path, string(content))
}
}
return ""
}
Assembly order with global instructions:
[system message]
1. staticPrompt
2. envBlock()
3. globalInstructions() ← user-level preferences
4. projectInstructions() ← project-level conventions
[user / assistant history]
Pros: Lets users set personal preferences once (e.g. "I prefer functional style", "always use
pnpm not npm") that apply across all projects.
Cons: Introduces a home-directory dependency; needs documentation so users know the file
exists; global instructions could conflict with project instructions.
Idea 9 — compact.md: Context Window Compression Prompt¶
Approach: Following kimi-cli's compact.md, add a dedicated prompt and a /compact REPL
command that summarises the conversation history into a structured block, then replaces the full
message history with that summary to reclaim context window space.
const compactPrompt = `Summarise the conversation so far into a structured block:
<summary>
<task>One sentence: what the user is trying to achieve.</task>
<done>Bullet list: what has been completed successfully.</done>
<state>Current state of the codebase: what was changed, where.</state>
<next>What remains to be done.</next>
<decisions>Any important decisions or constraints the user expressed.</decisions>
</summary>
Output ONLY the <summary> block. No other text.`
When the user types /compact:
1. Append the compact prompt as a user message
2. Get the model's <summary> response
3. Replace AppState.messages with a single synthetic user message containing the summary
4. Continue the session — the model now has a compressed history
Pros: Directly solves context window exhaustion on long coding sessions without losing the
essential thread of what has been done; XML structure makes the summary machine-parseable for
future features.
Cons: Some context fidelity is lost; requires a new REPL command; the summary quality depends
on the model.
Summary Table¶
| # | Idea | Complexity | Value | Status |
|---|---|---|---|---|
| 1 | Single embedded static prompt | Low | High | Superseded by Ideas 2+3 |
| 2 | Static prompt + dynamic env block | Low | Very High | Planned — see output-3 |
| 3 | AGENTS.md auto-loading |
Low | Very High | Planned — see output-3 |
| 4 | Per-provider system prompts | Medium | Medium | Deferred |
| 5 | Structured multi-section prompt | Low | Medium | Incorporated into Ideas 2+3 |
| 6 | Plan mode via <system-reminder> |
Medium | High | Future feature |
| 7 | AGENTS.md self-update reminder |
Very Low | Medium | Include in Ideas 2+3 impl |
| 8 | Global ~/.keen/AGENTS.md |
Low | Medium | Deferred |
| 9 | compact.md context compression |
Medium | High | Future feature |
Recommended Implementation Order¶
- Ideas 2 + 3 (static + env +
AGENTS.md) — the highest-leverage change; full plan inoutput-3_system-prompt.md. - Idea 7 (
AGENTS.mdself-update reminder) — a one-line addition to Ideas 2+3 implementation; near-zero cost. - Idea 8 (global
~/.keen/AGENTS.md) — low complexity, rounds out the instruction loading story. - Idea 6 (plan mode) — compelling UX feature once the base is solid.
- Idea 9 (
/compact) — important for long sessions; implement when context exhaustion becomes a real user complaint. - Idea 4 (per-model routing) — implement only once a concrete behavioural difference is observed and measured; premature optimisation otherwise.