Before the model ever processes your first message, Claude Code compiles a system prompt that can span thousands of tokens. This isn't a single static string baked into the binary — it's assembled fresh each session from a pipeline of composable sections, some cached across turns, others recomputed on every request, and all of them shaped by the active tools, user settings, CLAUDE.md files, MCP servers, and feature flags present at runtime.
constants/prompts.ts → utils/systemPrompt.ts →
constants/systemPromptSections.ts → utils/claudemd.ts →
memdir/memdir.ts
The architecture separates concerns cleanly: prompts.ts owns the
content of every section; systemPrompt.ts owns the
priority logic that decides which prompt even runs; and
systemPromptSections.ts owns the caching registry that prevents
redundant recomputation between turns.
Priority resolver
systemPrompt.ts — override → coordinator → agent → custom → default
Content factory
prompts.ts — static + dynamic sections assembled per session
Section registry
systemPromptSections.ts — memoized vs. volatile section cache
CLAUDE.md loader
claudemd.ts — multi-scope file discovery, @include, frontmatter
Memory system
memdir/memdir.ts — MEMORY.md + auto-memory injection
Cache boundary
SYSTEM_PROMPT_DYNAMIC_BOUNDARY — global-scope prefix split
buildEffectiveSystemPrompt() in utils/systemPrompt.ts is the first
gate. It implements a strict priority waterfall. Once a higher-priority source is found,
lower layers are skipped entirely.
appendSystemPrompt is the only thing that always appends — it is added to
every branch except when overrideSystemPrompt is active. This is how
--append-system-prompt works regardless of what mode you're in.
In proactive / KAIROS mode the agent's instructions are appended to the default prompt rather than replacing it. This mirrors the "teammate" pattern: the proactive default is already a lean autonomous-agent identity, and domain agents add on top.
// utils/systemPrompt.ts — proactive mode branch
if (agentSystemPrompt && (feature('PROACTIVE') || feature('KAIROS')) && isProactiveActive()) {
return asSystemPrompt([
...defaultSystemPrompt,
`\n# Custom Agent Instructions\n${agentSystemPrompt}`,
...(appendSystemPrompt ? [appendSystemPrompt] : []),
])
}
getSystemPrompt() in constants/prompts.ts is the main content
factory. It builds the defaultSystemPrompt array that feeds into the priority
resolver. The function has a fast escape hatch: if CLAUDE_CODE_SIMPLE=1 it
returns a three-line stub and exits immediately.
For normal sessions it gathers four pieces of runtime state in parallel, then assembles two halves — a static half and a dynamic half — separated by a boundary marker that controls prompt-cache scoping.
// constants/prompts.ts — abbreviated assembly
export async function getSystemPrompt(tools, model, additionalDirs, mcpClients) {
const [skillToolCommands, outputStyleConfig, envInfo] = await Promise.all([
getSkillToolCommands(cwd),
getOutputStyleConfig(),
computeSimpleEnvInfo(model, additionalDirs),
])
return [
// ── Static (globally cacheable) ──
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
getSimpleDoingTasksSection(), // code-style, YAGNI, security rules
getActionsSection(), // reversibility / blast-radius guidance
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// ── Boundary marker ──
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
// ── Dynamic (session-specific, registry-managed) ──
...resolvedDynamicSections, // session_guidance, memory, env_info_simple, ...
].filter(s => s !== null)
}
Static sections — what they contain
| Section function | What it instills | Scope |
|---|---|---|
getSimpleIntroSection() |
Identity ("interactive agent for software engineering tasks"), URL-generation guard, CYBER_RISK_INSTRUCTION | static |
getSimpleSystemSection() |
Markdown rendering, permission-mode tool approval, system-reminder tags, prompt-injection warning, hooks guidance, auto-compression notice | static |
getSimpleDoingTasksSection() |
Task scope ("software engineering tasks"), YAGNI/no-gold-plating rules, no speculative abstractions, security hygiene, code-style bullets (ant-only extras) | static |
getActionsSection() |
Reversibility and blast-radius guidance — when to pause and confirm, risky action taxonomy (destructive, hard-to-reverse, visible-to-others) | static |
getUsingYourToolsSection() |
Prefer dedicated tools over Bash, REPL-mode path, parallel tool calls, task-tracking tool | static |
getSimpleToneAndStyleSection() |
No emojis, file:line references, GitHub issue format, no colon before tool calls | static |
getOutputEfficiencySection() |
Conciseness / prose quality rules — ant build gets a rich prose guide, external gets brief "go straight to the point" | static |
process.env.USER_TYPE === 'ant'.
These are compiled out of the external binary via Bun dead-code elimination.
Every // @[MODEL LAUNCH] comment marks instructions that need updating at each new model release.
Everything after the boundary marker is managed through the section registry
defined in constants/systemPromptSections.ts. Each section is either
memoized (computed once per session, cached until /clear or /compact)
or volatile (recomputed every turn).
// systemPromptSections.ts
export function systemPromptSection(name, compute): SystemPromptSection {
return { name, compute, cacheBreak: false } // memoized
}
export function DANGEROUS_uncachedSystemPromptSection(name, compute, reason) {
return { name, compute, cacheBreak: true } // recomputes every turn
}
resolveSystemPromptSections() iterates the registry, returns cached values for
non-cacheBreak sections, and calls compute() for volatile or uncached ones.
Cache entries are keyed by the section's name string and stored in
bootstrap/state.ts so the React render cycle can't lose them.
| Section name | Content | Cache behaviour |
|---|---|---|
session_guidance |
Ask-user-question guidance, interactive shell tip, agent-tool guidance, skill invocation syntax, verification-agent contract | memoized |
memory |
CLAUDE.md hierarchy + MEMORY.md auto-memory (see §5) | memoized |
ant_model_override |
Internal ant build suffix injected from remote config | memoized |
env_info_simple |
CWD, git status, platform, shell, OS version, model name, knowledge cutoff, model family reference | memoized |
language |
Language preference from settings → "Always respond in X" | memoized |
output_style |
Named output style loaded from settings (name + prompt string) | memoized |
mcp_instructions |
Per-server instructions blocks from connected MCP servers |
volatile — MCP servers connect/disconnect between turns |
scratchpad |
Per-session scratchpad directory path and usage rules | memoized |
frc |
Function-result-clearing notice (CACHED_MICROCOMPACT feature) | memoized |
token_budget |
"Keep working until you approach the target" instruction | memoized |
Claude's API supports prompt caching: identical prefixes are billed at a fraction of normal input-token cost. To exploit this, the system prompt must be split into a stable prefix (same across all users) and a volatile tail (per-session content).
// constants/prompts.ts
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
// WARNING: Do not remove or reorder this marker without updating cache logic in:
// - src/utils/api.ts (splitSysPromptPrefix)
// - src/services/api/claude.ts (buildSystemPromptBlocks)
Everything before the marker can use scope: 'global' in the API call
— cacheable across all organisations. Everything after contains session-specific
content (your CWD, git status, CLAUDE.md files, model ID) and must not be globally cached.
getSessionSpecificGuidanceSection — which lives after the boundary — for
exactly this reason. Comments reference "PR #24490, #24171" for the same bug class.
The memory section is where every CLAUDE.md file the user has placed in their
filesystem gets loaded. utils/claudemd.ts discovers and ranks files across
four scopes, loading them in priority order (lower-priority first, higher-priority last —
so the model attends more to the files it sees last).
// utils/claudemd.ts — load order comment
// 1. Managed memory (/etc/claude-code/CLAUDE.md) — Global for all users on machine
// 2. User memory (~/.claude/CLAUDE.md) — Private global for all projects
// 3. Project memory (CLAUDE.md / .claude/CLAUDE.md — Checked into codebase
// .claude/rules/*.md)
// 4. Local memory (CLAUDE.local.md) — Private project-specific
CLAUDE.md,
.claude/CLAUDE.md, and every .md file in .claude/rules/.
Files closer to CWD appear later in the array — giving them higher effective priority.
The @include directive
CLAUDE.md files can reference other files with an @path directive placed
in leaf text (not inside code blocks). Supported forms:
# CLAUDE.md
@shared-rules.md # relative path (same dir)
@./scripts/lint-conventions.md # explicit relative
@~/company/global-standards.md # home-relative
@/absolute/path/to/rules.md # absolute
Included files are inserted as separate entries before the including file. Circular references are prevented via a processed-path set. Non-existent targets are silently skipped. The loader restricts includes to a large allowlist of text file extensions — binary files (images, PDFs) are rejected to prevent accidental context bloat.
Frontmatter path filtering
A CLAUDE.md file can carry YAML frontmatter with a paths key. Only files
under those glob patterns will receive the instructions. This lets you place
language-specific or directory-specific rules in a single CLAUDE.md without polluting
every project.
---
paths:
- src/components/**
- "*.tsx"
---
Always use named exports in React components.
The memory instruction wrapper
Every loaded memory file is wrapped in a header injected by claudemd.ts:
const MEMORY_INSTRUCTION_PROMPT =
'Codebase and user instructions are shown below. Be sure to adhere to these ' +
'instructions. IMPORTANT: These instructions OVERRIDE any default behavior and ' +
'you MUST follow them exactly as written.'
This is the preamble you see in your own <system-reminder> context.
Individual file contents are separated by clearly labeled blocks identifying each scope
(user memory, project memory, local memory) so the model can reason about origin.
MEMORY.md auto-memory
memdir/memdir.ts handles a separate auto-memory system layered on top.
When enabled, a per-session MEMORY.md file is also injected. The file is
truncated to 200 lines or 25,000 bytes, whichever fires
first, with a warning appended if truncated. This prevents an ever-growing memory file
from bloating every request.
// memdir/memdir.ts
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
The env_info_simple section is assembled by computeSimpleEnvInfo().
It gives the model a snapshot of the runtime context it's operating in.
// Produced output (representative)
# Environment
You have been invoked in the following environment:
- Primary working directory: /Users/alice/myproject
- Is a git repository: true
- Platform: darwin
- Shell: zsh
- OS Version: Darwin 25.3.0
- You are powered by the model named Claude Sonnet 4.6. The exact model ID is claude-sonnet-4-6.
- Assistant knowledge cutoff is August 2025.
- The most recent Claude model family is Claude 4.5/4.6 ...
- Claude Code is available as a CLI in the terminal, desktop app ...
- Fast mode for Claude Code uses the same Claude Opus 4.6 model with faster output ...
Shell detection parses process.env.SHELL to extract the name. On Windows,
it appends a note to prefer Unix shell syntax (/dev/null not NUL,
forward slashes in paths). Knowledge cutoff dates are hardcoded per model canonical name via
getKnowledgeCutoff() — every model launch requires a
// @[MODEL LAUNCH] update.
isUndercover() active, all
model name references are stripped from the environment section. This prevents internal
model IDs from leaking into public commits, PRs, or screenshots.
When MCP servers are connected, getMcpInstructions() iterates the
ConnectedMCPServer objects and assembles a block from any that expose
an instructions field.
// constants/prompts.ts
function getMcpInstructions(mcpClients) {
const withInstructions = mcpClients
.filter(c => c.type === 'connected')
.filter(c => c.instructions)
const blocks = withInstructions.map(c => `## ${c.name}\n${c.instructions}`).join('\n\n')
return `# MCP Server Instructions\n\n...\n\n${blocks}`
}
This is what injects text like "IMPORTANT: Before using any chrome browser tools, you MUST
first load them using ToolSearch" into the system prompt — it comes verbatim from the MCP
server's instructions field, not from Claude Code's own code.
There is also an experimental mcpInstructionsDelta path: when that feature is
enabled, instructions are delivered as persisted attachment objects rather than
being re-injected into the system prompt every turn. This avoids the prompt-cache bust that
happens when a late-connecting MCP server forces a new system prompt hash.
Subagents launched by AgentTool do not go through getSystemPrompt().
They start with whatever system prompt the caller passes — typically DEFAULT_AGENT_PROMPT.
enhanceSystemPromptWithEnvDetails() then appends environment context and
agent-specific notes.
// The agent default prompt — what every subagent starts with
export const DEFAULT_AGENT_PROMPT = `You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's message, you should use the tools available to complete the task. Complete the task fully—don't gold-plate, but don't leave it half-done. When you complete the task, respond with a concise report covering what was done and any key findings — the caller will relay this to the user, so it only needs the essentials.`
// Notes appended by enhanceSystemPromptWithEnvDetails()
`Notes:
- Agent threads always have their cwd reset between bash calls — use absolute file paths.
- In your final response, share file paths (always absolute, never relative) ...
- For clear communication the assistant MUST avoid using emojis.
- Do not use a colon before tool calls.`
computeEnvInfo() (the fuller version, not computeSimpleEnvInfo)
is used here — it adds uname -sr output via getUnameSR() and
wraps the env block in <env> XML tags rather than the simpler bullet-list
format used in the main session.
CLAUDE_CODE_SIMPLE=1
Setting this environment variable activates a three-line system prompt — no instructions, no CLAUDE.md, no tool guidance, just CWD and date. Useful for benchmarking raw model capability without Claude Code's prompt overhead.
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`]
}
Proactive / KAIROS mode
When feature('PROACTIVE') or feature('KAIROS') is enabled and
proactive mode is active, getSystemPrompt() returns a short autonomous-agent
identity prompt instead of the full interactive-session prompt. It includes memory,
env info, MCP instructions, and the proactive section — but none of the interactive
coding-task guidance.
return [
`\nYou are an autonomous agent. Use the available tools to do useful work.\n\n${CYBER_RISK_INSTRUCTION}`,
getSystemRemindersSection(),
await loadMemoryPrompt(),
envInfo,
getLanguageSection(settings.language),
getMcpInstructionsSection(mcpClients),
getScratchpadInstructions(),
getProactiveSection(),
].filter(s => s !== null)
Key Takeaways
buildEffectiveSystemPrompt()implements a strict priority waterfall: override > coordinator > agent > custom > default, withappendSystemPromptalways appending.- The prompt is split into a static half (globally cache-scoped, same for all users) and a dynamic tail (session-specific). The boundary marker
__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__is the seam. - Dynamic sections are managed by a named registry. Most are memoized for the session; only
mcp_instructionsis volatile because servers connect mid-session. - CLAUDE.md files are loaded in four scopes (managed → user → project → local), with files closer to CWD taking higher priority by appearing last.
- The
@includedirective lets CLAUDE.md files compose other files. Frontmatterpathsgates instructions to specific file patterns. - Subagents bypass
getSystemPrompt()entirely, starting fromDEFAULT_AGENT_PROMPTthen enhanced with env details. - Multiple escape hatches exist:
CLAUDE_CODE_SIMPLE=1for a stub prompt, proactive mode for an autonomous-agent identity, undercover mode to strip all model name references. - Every
// @[MODEL LAUNCH]comment marks code that needs human attention at each new Claude model release.
Knowledge Check
overrideSystemPrompt is set in buildEffectiveSystemPrompt()?mcp_instructions marked as DANGEROUS_uncachedSystemPromptSection while other dynamic sections are memoized?__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__?