Why this matters
Claude Code can get prompt text from several places, and the order matters. Sometimes a caller wants to replace the default prompt entirely. Sometimes the runtime should use a coordinator prompt or an agent-specific prompt. If none of those apply, the normal default prompt still needs to be assembled and then optionally extended with extra text at the end.
Big picture first
This page follows the real priority rules in buildEffectiveSystemPrompt(...),
then shows how fetchSystemPromptParts(...) decides which inputs to fetch, and
finally shows the QueryEngine.ts assembly path that turns the selected pieces
into the branded SystemPrompt array.
Final system prompt container
Claude Code does not hand one string to the model. It hands a branded array of prompt sections.
| Field | Meaning | First Pass |
|---|---|---|
defaultSystemPrompt | The array returned by getSystemPrompt(...) before override logic is applied. | Treat this as the default prompt candidate. |
customSystemPrompt | A caller-provided replacement prompt. | If this exists, the default builder path is skipped in fetchSystemPromptParts(...). |
appendSystemPrompt | Extra prompt text appended at the end of the chosen prompt. | This is the simplest override to understand. |
SystemPrompt | The branded readonly string array created by asSystemPrompt([...]). | This is the object that later API calls consume. |
The short version is simple: overrideSystemPrompt wins first, coordinator
mode can replace the normal prompt next, agent prompts can win after that, and
the customSystemPrompt !== undefined path only exists for callers that passed
their own system prompt. If none of those special cases apply, Claude Code uses
the default prompt and then appends anything extra at the end.
The priority rules
/**
* Builds the effective system prompt array based on priority:
* 0. Override system prompt (if set, e.g., via loop mode - REPLACES all other prompts)
* 1. Coordinator system prompt (if coordinator mode is active)
* 2. Agent system prompt (if mainThreadAgentDefinition is set)
* - In proactive mode: agent prompt is APPENDED to default (agent adds domain
* instructions on top of the autonomous agent prompt, like teammates do)
* - Otherwise: agent prompt REPLACES default
* 3. Custom system prompt (if specified via --system-prompt)
* 4. Default system prompt (the standard Claude Code prompt)
*
* Plus appendSystemPrompt is always added at the end if specified (except when override is set).
*/
export function buildEffectiveSystemPrompt({
mainThreadAgentDefinition,
toolUseContext,
customSystemPrompt,
defaultSystemPrompt,
appendSystemPrompt,
overrideSystemPrompt,
}: {
mainThreadAgentDefinition: AgentDefinition | undefined
toolUseContext: Pick<ToolUseContext, 'options'>
customSystemPrompt: string | undefined
defaultSystemPrompt: string[]
appendSystemPrompt: string | undefined
overrideSystemPrompt?: string | null
}): SystemPrompt {
if (overrideSystemPrompt) {
return asSystemPrompt([overrideSystemPrompt])
}
if (
feature('COORDINATOR_MODE') &&
isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) &&
!mainThreadAgentDefinition
) {
const { getCoordinatorSystemPrompt } =
require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js')
return asSystemPrompt([
getCoordinatorSystemPrompt(),
...(appendSystemPrompt ? [appendSystemPrompt] : []),
])
}
const agentSystemPrompt = mainThreadAgentDefinition
? isBuiltInAgent(mainThreadAgentDefinition)
? mainThreadAgentDefinition.getSystemPrompt({
toolUseContext: { options: toolUseContext.options },
})
: mainThreadAgentDefinition.getSystemPrompt()
: undefined
That branch order is the key teaching point. The mainThreadAgentDefinition
path can replace the default prompt, while overrideSystemPrompt short-circuits
the whole function before anything else gets a chance to run.
The fetch helper
export async function fetchSystemPromptParts({
tools,
mainLoopModel,
additionalWorkingDirectories,
mcpClients,
customSystemPrompt,
}: {
tools: Tools
mainLoopModel: string
additionalWorkingDirectories: string[]
mcpClients: MCPServerConnection[]
customSystemPrompt: string | undefined
}): Promise<{
defaultSystemPrompt: string[]
userContext: { [k: string]: string }
systemContext: { [k: string]: string }
}> {
const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([
customSystemPrompt !== undefined
? Promise.resolve([])
: getSystemPrompt(
tools,
mainLoopModel,
additionalWorkingDirectories,
mcpClients,
),
getUserContext(),
customSystemPrompt !== undefined ? Promise.resolve({}) : getSystemContext(),
])
return { defaultSystemPrompt, userContext, systemContext }
}
fetchSystemPromptParts(...) is deliberately conservative. When
customSystemPrompt !== undefined, it skips the default prompt builder and the
system-context fetch because the caller already chose a replacement prompt.
That keeps the cache-key prefix aligned with the real runtime behavior.
The QueryEngine assembly path
const customPrompt =
typeof customSystemPrompt === 'string' ? customSystemPrompt : undefined
const {
defaultSystemPrompt,
userContext: baseUserContext,
systemContext,
} = await fetchSystemPromptParts({
tools,
mainLoopModel: initialMainLoopModel,
additionalWorkingDirectories: Array.from(
initialAppState.toolPermissionContext.additionalWorkingDirectories.keys(),
),
mcpClients,
customSystemPrompt: customPrompt,
})
const userContext = {
...baseUserContext,
...getCoordinatorUserContext(
mcpClients,
isScratchpadEnabled() ? getScratchpadDir() : undefined,
),
}
const memoryMechanicsPrompt =
customPrompt !== undefined && hasAutoMemPathOverride()
? await loadMemoryPrompt()
: null
const systemPrompt = asSystemPrompt([
...(customPrompt !== undefined ? [customPrompt] : defaultSystemPrompt),
...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []),
...(appendSystemPrompt ? [appendSystemPrompt] : []),
])
This is the last step in the chain. QueryEngine.ts only injects
memoryMechanicsPrompt when a custom prompt is present and
hasAutoMemPathOverride() is true, which keeps memory mechanics tied to the
explicit auto-memory override path instead of every prompt build.
Takeaways
- Prompt precedence is explicit: override, coordinator, agent, custom, then default.
- fetchSystemPromptParts(...) skips work when a custom prompt already replaces the default path.
- The final SystemPrompt is a branded array built by asSystemPrompt([...]), not a free-form string.