System Prompt Assembly

Description

The system prompt in Claude Code is not a static string — it is a compiled artifact assembled from multiple layers on every conversational turn, with a critical cache boundary dividing static from dynamic sections. Before the user types anything, Claude Code compiles 15,000-40,000 tokens of hidden context including identity instructions, tool descriptions, CLAUDE.md rules, git status, memory, and system reminders.

Assembly pipeline

Entry point: getSystemPrompt() in src/constants/prompts.ts (lines 444-577)

Static sections (cacheable)

Assembled in order: 1. getSimpleIntroSection() — main identity statement ("You are Claude Code...") 2. getSimpleSystemSection() — tool use and permissions guidance 3. getSimpleDoingTasksSection() — software engineering task guidance 4. getActionsSection() — risk assessment and confirmation rules 5. getUsingYourToolsSection() — tool preference guidance (Read over cat, Edit over sed, etc.) 6. getSimpleToneAndStyleSection() — communication style

Cache boundary

STATIC SECTION (cached globally, shared across sessions)
────────────── __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ ──────────────
DYNAMIC SECTION (session-specific, changes per turn)

SYSTEM_PROMPT_DYNAMIC_BOUNDARY (line 114-115) splits the prompt. Everything above the boundary is globally cacheable (byte-identical across all sessions). Everything below is recomputed per turn or per session. The boundary is only inserted when shouldUseGlobalCacheScope() is true (1P API only).

Dynamic sections (registry-managed)

Registered as SystemPromptSection objects with caching (src/constants/systemPromptSections.ts):

Standard cached sections (computed once per session, cached until /clear or /compact): - session_guidance — session-specific tool recommendations - memory — loaded memory prompt from memdir - ant_model_override — internal-only model-specific instructions - env_info_simple — environment details (CWD, git status, platform, model, shell) - language — language preference - output_style — output style configuration - scratchpad — scratchpad directory instructions (if enabled) - frc — function result clearing instructions (feature-gated) - summarize_tool_results — tool result summary guidance - numeric_length_anchors — length limits (internal-only) - token_budget — token target tracking (feature-gated) - brief — brief mode section (Kairos feature)

Cache-breaking sections (recomputed every turn): - mcp_instructions — MCP server instructions. Uses DANGEROUS_uncachedSystemPromptSection() because MCP servers can connect/disconnect between turns.

The DANGEROUS_ naming convention

Any function that forces cache invalidation is prefixed with DANGEROUS_:

DANGEROUS_uncachedSystemPromptSection(name, computeFn, reason: string)

The reason parameter is mandatory — it documents why cache invalidation is necessary. This naming convention makes cache-breaking visible in code review and grep.

Section caching mechanism

src/constants/systemPromptSections.ts — two section types:

resolveSystemPromptSections() checks the cache for each section, recomputes on miss or if cacheBreak: true, and stores back to cache.

Cache boundary at the API level

src/utils/api.ts (lines 321-435, splitSysPromptPrefix()) splits the prompt into cache blocks:

With global cache (1P API, boundary found): up to 4 blocks: 1. Attribution header (no cache) 2. System prompt prefix (no cache) 3. Static content before boundary (cacheScope: 'global') — shared across all organizations 4. Dynamic content after boundary (no cache)

With MCP tools (skipGlobalCache): 3 blocks with org-level caching: 1. Attribution header (no cache) 2. System prompt prefix (cacheScope: 'org') 3. Everything else (cacheScope: 'org')

Default (3P providers): 3 blocks with org-level caching.

Tool pool assembly

src/tools.ts (lines 345-367, assembleToolPool()) — combines built-in and MCP tools with cache-stable ordering:

const byName = (a, b) => a.name.localeCompare(b.name)
return uniqBy(
  [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
  'name',
)

Key design: built-in tools form a contiguous prefix, sorted alphabetically. MCP tools follow, also sorted alphabetically. This partition boundary is critical — the server's claude_code_system_cache_policy places a global cache breakpoint after the last built-in tool. A flat sort would interleave MCP tools into built-ins, invalidating all downstream cache keys.

On name conflict, uniqBy preserves insertion order, so built-in tools win.

System reminders

40+ short behavioral instructions injected as <system-reminder> tags into user messages, not the system prompt. This preserves the prompt cache — adding a reminder to a user message doesn't invalidate the static system prompt cache.

Injection point: prependUserContext() in src/utils/api.ts (lines 449-474). Context sources: - System context (src/context.ts:116-150): git status, optional cache breaker - User context (src/context.ts:155-189): CLAUDE.md content, current date

Applied in the query flow: user context prepended to messages, system context appended to system prompt.

Ant-only two-tier prompt

Internal Anthropic employees (process.env.USER_TYPE === 'ant') receive a materially different system prompt: - Collaborative and verification-focused (vs terse, task-oriented for external) - Deeper code style guidance (comment reduction, verification before completion) - False-claims mitigation (faithfully report outcomes, don't hide failures) - Numeric length anchors for output tokens - Undercover mode suppression (hide model names in internal repos) - Three-layer verification instructions (internal-only)

Proactive mode variant

When feature('PROACTIVE') is active (src/constants/prompts.ts:467-489), the system prompt is replaced with a lean autonomous agent prompt: - Autonomous work instructions (tick handling, pacing) - Sleep tool guidance - First wake-up greeting requirements - Bias toward autonomous action - Skips most standard static sections

Hidden context scale

Before the user types anything:

Component Tokens (approx)
System prompt (static) 5,000-10,000
CLAUDE.md (up to 40K chars) 2,000-8,000
Git status, file diffs 1,000-5,000
IDE selections, LSP diagnostics 500-2,000
Auto-memory index (200 lines) 500-1,500
System reminders (40+) 1,000-3,000
Sonnet memory side-query result 500-2,000

Three models run per interaction: the main model, a Sonnet memory side-query, and the two-stage auto-mode classifier.

What depends on it

Design trade-offs

Decision Trade-off
Global cache boundary Massive cost savings across all sessions, but static sections can never include session-specific content
Built-in/MCP tool partition Cache-stable ordering, but MCP tools can never appear in the cached prefix
System reminders in user messages Preserves prompt cache, but inflates every turn's input tokens
DANGEROUS_ naming convention Makes cache-breaking visible, but only enforced by convention (no build-time check)
Ant-only two-tier prompt Better experience for internal users, but two prompts to maintain
Section caching until /clear Reduces recomputation, but stale sections possible if external state changes mid-session

Key claims

Sources

src-20260409-a14e9e98c3cd, src-20260409-037a8abb6277, source code at src/constants/prompts.ts, src/constants/systemPromptSections.ts, src/utils/api.ts, src/tools.ts, src/context.ts