Skip to main content

GitHub Copilot SDK Deep Dive: Surgical system prompt customization

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 register
  • SystemMessageSection.CodeChangeRules : Rules around editing files and making code changes
  • SystemMessageSection.Identity : Agent identity preamble and mode statement
  • SystemMessageSection.ToolEfficiency : Tool usage patterns, parallel calling, batching guidelines
  • SystemMessageSection.EnvironmentContext : CWD, OS, git root, directory listing, available tools.
  • SystemMessageSection.Guidelines : General behavioral guidelines
  • SystemMessageSection.Safety : General behavioral guidelines
  • SystemMessageSection.ToolInstructions : Per-tool usage instructions
  • SystemMessageSection.CustomInstructions : Repository and organization custom instructions
  • SystemMessageSection.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 Replace when 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 Append when you want to bolt on a few extra instructions and you do not care where they land. Quick experiments, lightweight customizations, prototypes.
  • Use Customize when 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 Tone so the assistant sounds like a domain expert rather than a general-purpose coding assistant
  • Remove CodeChangeRules because your application does not involve code editing at all
  • Append to Guidelines with 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.

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

VS Code Planning mode

After the introduction of Plan mode in Visual Studio , it now also found its way into VS Code. Planning mode, or as I like to call it 'Hannibal mode', extends GitHub Copilot's Agent Mode capabilities to handle larger, multi-step coding tasks with a structured approach. Instead of jumping straight into code generation, Planning mode creates a detailed execution plan. If you want more details, have a look at my previous post . Putting plan mode into action VS Code takes a different approach compared to Visual Studio when using plan mode. Instead of a configuration setting that you can activate but have limited control over, planning is available as a separate chat mode/agent: I like this approach better than how Visual Studio does it as you have explicit control when plan mode is activated. Instead of immediately diving into execution, the plan agent creates a plan and asks some follow up questions: You can further edit the plan by clicking on ‘Open in Editor’: ...