This post is part of a follow-up series to my GitHub Copilot SDK blog series. After wrapping up the main series I was left with a list of features that deserved more than a passing mention. First up: SystemMessageConfig, and specifically the mode that most tutorials gloss right over.
The temptation of Replace
When you first discover that the Copilot SDK lets you control the system prompt, the obvious instinct is to reach for SystemMessageMode.Replace. Full control, clean slate, no surprises — what's not to like?
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Replace,
Content = "You are a helpful assistant."
}
});
However there is a big problem with adding this line. When you replace the system prompt wholesale you are not just customizing Copilot, you are evicting it. The carefully tuned defaults around tool use, safety, code quality expectations, and general behavior go out the window. For some use cases that is exactly what you want. For most real applications it is not.
The other common option is Append:
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Append,
Content = @"
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
"
}
Append is safe and simple. Your content lands at the end of Copilot's default prompt. The downside is that you have no control over what you are appending to. If you want to change how Copilot responds to tone-sensitive situations, or you want to remove a section that conflicts with your application's context, Append cannot help you.
That is where the third mode comes in.
Customize: the surgical option
SystemMessageMode.Customize lets you target individual named sections of the default system prompt and apply one of four actions to each: Replace, Remove, Append, or Prepend. Everything you do not touch stays exactly as it was.
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Customize,
Sections = new Dictionary<string, SectionOverride>
{
[SystemMessageSection.Tone] = new()
{
Action = SectionOverrideAction.Replace,
Content = "Respond in a warm, professional tone. Be thorough in explanations."
},
[SystemMessageSection.CodeChangeRules] = new()
{
Action = SectionOverrideAction.Remove
},
[SystemMessageSection.Guidelines] = new()
{
Action = SectionOverrideAction.Append,
Content = "\n* Always cite data sources"
},
},
Content = "Focus on financial analysis and reporting."
}
});
A few things worth noticing here:
The Content property at the top level still works. Even in Customize mode you can set a top-level Content alongside your Sections dictionary. Think of it as global additional context that does not target any specific section.
The actions are exactly what you would expect. Replace swaps the section content entirely. Remove strips it out. Append and Prepend let you inject content relative to what is already there — useful when you want to extend rather than override.
Unknown section IDs are handled gracefully. If you reference a section ID that the runtime does not recognize, the content is silently appended to additional instructions instead. Remove overrides on unknown IDs are silently ignored. This means your code will not blow up if the SDK evolves the prompt structure, though you may want to audit that behavior in production.
The available section IDs
The SDK exposes known sections as constants on SystemMessageSection. At the time of writing this post those include:
SystemMessageSection.Tone: The model's communication style and registerSystemMessageSection.CodeChangeRules: Rules around editing files and making code changesSystemMessageSection.Identity: Agent identity preamble and mode statementSystemMessageSection.ToolEfficiency: Tool usage patterns, parallel calling, batching guidelinesSystemMessageSection.EnvironmentContext: CWD, OS, git root, directory listing, available tools.SystemMessageSection.Guidelines: General behavioral guidelinesSystemMessageSection.Safety: General behavioral guidelinesSystemMessageSection.ToolInstructions: Per-tool usage instructionsSystemMessageSection.CustomInstructions: Repository and organization custom instructionsSystemMessageSection.RuntimeInstructions: Runtime-provided context and instructions (e.g. system notifications, memories, workspace context, mode-specific instructions, content-exclusion policy)SystemMessageSection.LastInstructions: End-of-prompt instructions: parallel tool calling, persistence, task completion.
The exact set may grow as the SDK evolves. Worth checking the README for the current list before you commit to a Remove on something that moved.
When to use each mode
Here is my practical take on how to choose:
- Use
Replacewhen you are building something that is decidedly not GitHub Copilot — a domain-specific agent with its own identity, tool set, and behavioral contract. You are trading the defaults for full ownership. - Use
Appendwhen you want to bolt on a few extra instructions and you do not care where they land. Quick experiments, lightweight customizations, prototypes. - Use
Customizewhen you are building a real application on top of Copilot and you want a specific, targeted behavioral change. You know which section you want to affect and you want the rest of the defaults preserved. This is the production-grade option.
A concrete example where Customize shines: suppose you are embedding the SDK in a financial analysis tool. You probably want to:
- Replace
Toneso the assistant sounds like a domain expert rather than a general-purpose coding assistant - Remove
CodeChangeRulesbecause your application does not involve code editing at all - Append to
Guidelineswith your own compliance and citation requirements
Doing that with Replace means manually reconstructing everything you did not want to change. Doing it with Append means piling instructions on top of defaults that may actively contradict them. Customize threads the needle.
Putting it together: a practical example
Below is how that financial analysis scenario might look in practice:
await using var client = new CopilotClient();
await client.StartAsync();
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = PermissionHandler.ApproveAll,
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Customize,
// Top-level content adds domain context without targeting a specific section
Content = "You are embedded in FinSight, a financial analysis platform. " +
"Users are finance professionals who expect precise, sourced answers.",
Sections = new Dictionary<string, SectionOverride>
{
// Replace the tone entirely — we want analyst, not assistant
[SystemMessageSection.Tone] = new()
{
Action = SectionOverrideAction.Replace,
Content = "Adopt the communication style of a senior financial analyst. " +
"Be precise, cite figures, and flag uncertainty explicitly."
},
// This is a read/analyze tool, not a code editor
[SystemMessageSection.CodeChangeRules] = new()
{
Action = SectionOverrideAction.Remove
},
// Extend the guidelines with compliance requirements
[SystemMessageSection.Guidelines] = new()
{
Action = SectionOverrideAction.Append,
Content = "\n* Always cite the data source or document when referencing figures" +
"\n* Flag when a statement is an estimate or projection" +
"\n* Do not make investment recommendations"
}
}
}
});
Clean, explicit, and auditable. Anyone reading this code can understand exactly what was changed and why, without having to mentally diff a full system prompt string.