Studio Notebook

Claude Code Atlas

Default System Prompt And Dynamic Sections

Learn how Claude Code builds the default system prompt from static sections, dynamic sections, and a cache boundary.

Why this matters

The default system prompt is not a single giant string literal. It is built from named sections, and some of those sections are cached while others are deliberately allowed to change from turn to turn.

Big picture first

Read this page in three layers:

  1. the plain-English prompt text the model actually sees
  2. the getSystemPrompt(...) builder that assembles the prompt list
  3. the section registry helpers that decide what gets cached and what does not

The two prompt excerpts below are both real, but they are not adjacent in the assembled runtime prompt. Claude Code inserts several whole sections between them when getSystemPrompt(…) builds the final list.

A real intro-section excerpt

constants/prompts.ts

You are an interactive agent that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.

IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.

A real tone-and-style-section excerpt

constants/prompts.ts

# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
- Your responses should be short and concise.
- When referencing specific functions or pieces of code include the pattern file_path:line_number to allow the user to easily navigate to the source code location.
- When referencing GitHub issues or pull requests, use the owner/repo#123 format (e.g. anthropics/claude-code#100) so they render as clickable links.
- Do not use a colon before tool calls. Your tool calls may not be shown directly in the output, so text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period.

These excerpts matter because they show the source has distinct sections with different jobs. The intro sets the model’s overall posture, and the tone block adds style guidance later in the assembled prompt.

The builder

Here is the part of getSystemPrompt(...) that matters for this chapter:

export async function getSystemPrompt(
  tools: Tools,
  model: string,
  additionalWorkingDirectories?: string[],
  mcpClients?: MCPServerConnection[],
): Promise<string[]> {
const dynamicSections = [
  systemPromptSection('session_guidance', () =>
    getSessionSpecificGuidanceSection(enabledTools, skillToolCommands),
  ),
  systemPromptSection('memory', () => loadMemoryPrompt()),
  systemPromptSection('ant_model_override', () =>
    getAntModelOverrideSection(),
  ),
  systemPromptSection('env_info_simple', () =>
    computeSimpleEnvInfo(model, additionalWorkingDirectories),
  ),
  systemPromptSection('language', () =>
    getLanguageSection(settings.language),
  ),
  systemPromptSection('output_style', () =>
    getOutputStyleSection(outputStyleConfig),
  ),
  DANGEROUS_uncachedSystemPromptSection(
    'mcp_instructions',
    () =>
      isMcpInstructionsDeltaEnabled()
        ? null
        : getMcpInstructionsSection(mcpClients),
    'MCP servers connect/disconnect between turns',
  ),
  systemPromptSection('scratchpad', () => getScratchpadInstructions()),
  systemPromptSection('frc', () => getFunctionResultClearingSection(model)),
  systemPromptSection(
    'summarize_tool_results',
    () => SUMMARIZE_TOOL_RESULTS_SECTION,
  ),
]

const resolvedDynamicSections =
  await resolveSystemPromptSections(dynamicSections)

return [
  getSimpleIntroSection(outputStyleConfig),
  getSimpleSystemSection(),
  outputStyleConfig === null ||
  outputStyleConfig.keepCodingInstructions === true
    ? getSimpleDoingTasksSection()
    : null,
  getActionsSection(),
  getUsingYourToolsSection(enabledTools),
  getSimpleToneAndStyleSection(),
  getOutputEfficiencySection(),
  ...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
  ...resolvedDynamicSections,
].filter(s => s !== null)

The key idea is simple: the prompt starts with the static sections, then optionally inserts SYSTEM_PROMPT_DYNAMIC_BOUNDARY, and only then appends the resolved dynamic tail. That is why the chapter talks about “assembled sections” instead of one monolithic prompt string.

The section registry

The helper file makes the caching model explicit:

type ComputeFn = () => string | null | Promise<string | null>

type SystemPromptSection = {
  name: string
  compute: ComputeFn
  cacheBreak: boolean
}

export function systemPromptSection(
  name: string,
  compute: ComputeFn,
): SystemPromptSection {
  return { name, compute, cacheBreak: false }
}

export function DANGEROUS_uncachedSystemPromptSection(
  name: string,
  compute: ComputeFn,
  _reason: string,
): SystemPromptSection {
  return { name, compute, cacheBreak: true }
}

export async function resolveSystemPromptSections(
  sections: SystemPromptSection[],
): Promise<(string | null)[]> {
  const cache = getSystemPromptSectionCache()

  return Promise.all(
    sections.map(async s => {
      if (!s.cacheBreak && cache.has(s.name)) {
        return cache.get(s.name) ?? null
      }
      const value = await s.compute()
      setSystemPromptSectionCacheEntry(s.name, value)
      return value
    }),
  )
}

systemPromptSection(...) means “cache this if you can.” The dangerous helper means “recompute every turn on purpose.” resolveSystemPromptSections(...) then turns the registry into the actual prompt fragments that getSystemPrompt returns.

Why the boundary exists

SYSTEM_PROMPT_DYNAMIC_BOUNDARY is the line between stable prompt text and volatile prompt text. The runtime uses it so the cache can treat the static prefix differently from the dynamic tail. Put another way: the prompt is still one assembled result, but not every piece of it has the same lifetime.

Takeaways

  • The default system prompt is built from ordered sections, not handwritten as one block.
  • The boundary marker separates the stable prefix from the dynamic tail that gets resolved later.
  • Some sections are intentionally cached, while `DANGEROUS_uncachedSystemPromptSection(...)` opts out on purpose.