Skip to main content

Building Custom Copilot Extensions: A Developer's Guide

April 04, 2026 3 min read

Copilot Extensions let you build agents and skill providers that integrate directly into Copilot Chat. Users invoke your extension with @your-agent in VS Code, GitHub.com, or the CLI. Your extension receives conversation context, does its work, and streams a response back. Let's build one.

Extension Architecture

Two flavors: Agents handle full conversations invoked with @agent-name. Skill Providers expose discrete capabilities Copilot invokes automatically when relevant. Both are HTTP endpoints using SSE streaming in the OpenAI chat format.

Building an Agent in C#

Here's a deployment status agent that queries internal APIs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<DeploymentService>();
var app = builder.Build();

app.MapPost("/agent", async (HttpContext context, DeploymentService deployService) =>
{
    var request = await context.Request.ReadFromJsonAsync<CopilotRequest>();
    if (request is null) return Results.BadRequest();

    if (!VerifyGitHubSignature(context.Request, request))
        return Results.Unauthorized();

    var userMessage = request.Messages
        .LastOrDefault(m => m.Role == "user")?.Content ?? "";

    context.Response.ContentType = "text/event-stream";
    context.Response.Headers.CacheControl = "no-cache";

    var serviceName = ExtractServiceName(userMessage);
    if (serviceName != null)
    {
        var status = await deployService.GetStatusAsync(serviceName);
        await StreamResponse(context.Response, FormatDeploymentStatus(status));
    }
    else
    {
        var allServices = await deployService.GetAllServicesAsync();
        await StreamResponse(context.Response, FormatServiceList(allServices));
    }
    return Results.Empty;
});

app.Run();

The streaming response follows SSE format:

static async Task StreamResponse(HttpResponse response, string content)
{
    var chunks = SplitIntoChunks(content, maxChunkSize: 100);
    foreach (var chunk in chunks)
    {
        var sseData = new
        {
            choices = new[] { new {
                index = 0,
                delta = new { content = chunk },
                finish_reason = (string?)null
            }}
        };
        await response.WriteAsync($"data: {JsonSerializer.Serialize(sseData)}\n\n");
        await response.Body.FlushAsync();
    }
    await response.WriteAsync("data: [DONE]\n\n");
    await response.Body.FlushAsync();
}

Security: Request Verification

Every request includes a signature header — verification is mandatory:

static bool VerifyGitHubSignature(HttpRequest request, CopilotRequest body)
{
    if (!request.Headers.TryGetValue("X-GitHub-Signature-256", out var signatureHeader))
        return false;

    var secret = Environment.GetEnvironmentVariable("GITHUB_WEBHOOK_SECRET")!;
    var payload = JsonSerializer.Serialize(body);
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
    var expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();

    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(signatureHeader.ToString()),
        Encoding.UTF8.GetBytes(expected));
}

The X-GitHub-Token header represents the invoking user — use it to enforce access controls via the GitHub API.

Deployment and Registration

Deploy to Azure Container Apps for SSL, scaling, and cost-effective bursty workloads:

az acr build --registry myregistry --image copilot-deploy-agent:latest .
az containerapp create \
  --name copilot-deploy-agent \
  --resource-group copilot-extensions-rg \
  --environment copilot-env \
  --image myregistry.azurecr.io/copilot-deploy-agent:latest \
  --target-port 8080 --ingress external \
  --min-replicas 1 --max-replicas 5

Register in Settings → Developer settings → GitHub Apps, set the Copilot agent URL, and install in your organization. Members can then use @deploy-status in Copilot Chat.

Key Takeaways

  • Start with an agent, not a skill provider — agents are easier to reason about and test.
  • Verify every request — signature verification is your primary security boundary.
  • Use confirmation actions for write operations — never auto-execute destructive actions.
  • Deploy to a serverless platform — extensions are bursty by nature (active during work hours, idle overnight).
  • Invest in clear error messages — a helpful "not enough information" is better than a hallucinated answer.
Share this post

Comments

Ajit Gangurde

Software Engineer II at Microsoft | 15+ years in .NET & Azure