Skip to main content

GitHub Copilot SDK Deep Dive: Controlling built-in tools with toolset

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. This is the second post about the built-in tools.

When you create a session using the SDK, the agent has access to two distinct categories of tools:

  • Custom tools are the ones you define yourself — your CopilotTool.DefineTool(...) registrations, the available skills and MCP tools. These are the application-specific capabilities you build.
  • Built-in tools are what the Copilot CLI brings to the table out of the box: file reading, file writing, shell execution, web fetching, web search, and a handful of others. These power the agentic loop that makes Copilot useful without you having to implement everything from scratch.

By default, when you call CreateSessionAsync, both categories are available. And by default, built-in tools that could cause side effects — writing files, executing shell commands, fetching URLs — will prompt the user for permission before running, just as the CLI itself does when used interactively.

For a quick prototype that is fine. For a real application, you almost certainly want to make deliberate choices about which built-in tools are in scope.

Enter AvailableTools and ToolSet

The SessionConfig has an AvailableTools property that accepts a ToolSet. This is your control surface for deciding exactly what the model can reach for.

var session = await client.CreateSessionAsync(new()
{
    Model = "gpt-5.5",
    SystemMessage = systemMessageConfig,
    OnPermissionRequest = PermissionHandler.ApproveAll,
    AvailableTools = new ToolSet().AddCustom("*").AddBuiltIn("web_fetch"),
    Tools =
    [
        CopilotTool.DefineTool(SetCurrentPhase),
        CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }),
        CopilotTool.DefineTool(database.SearchProperties),
    ],
});

Let's break down what is happening here.

AddCustom("*") says: include all custom tools. In this case that means everything in the Tools array — SetCurrentPhase, ReportIntent, and database.SearchProperties — are all available to the model.

You could be more selective. AddCustom("SetCurrentPhase") would include only that specific tool. But "*" is the typical choice when you are registering tools because you want them used.

AddBuiltIn adds specific built-in tools by their string ID. Without it, the built-in set would be completely empty — you have already opted in to explicit control by instantiating a ToolSet yourself. With it, web_fetch is the only built-in the model can invoke.

The resulting toolset is additive: all your custom tools plus exactly web_fetch from the built-in catalog. Everything else — shell execution, file writing, file reading, web search — is not presented to the model at all. It cannot attempt to use them even if it would otherwise want to.

The built-in tool catalog

The built-in tool IDs mirror what you see in the Copilot CLI's --available-tools and --excluded-tools options. The ones you are most likely to care about in an SDK context:

Tool ID What it does
web_fetch Fetches the content of a URL
view Reads files from the filesystem
create Writes or creates files
edit Makes targeted edits to existing files
bash/powershell Executes shell commands

The shell tool in particular deserves a callout: it can do anything your process user can do. If your SDK application runs as a service account with broad permissions, granting the shell built-in to an agent session is a significant trust decision. The ToolSet pattern makes that choice explicit rather than implicit.

A full list of built-in tools can be found here: GitHub Copilot CLI command reference - GitHub Docs

Why this matters beyond security

The tool filtering is not just about preventing unintended side effects. It is also about model behavior.

When the model is aware of tools, it will try to use them when it thinks they are relevant. If web_search is available, the model may reach for it on questions it could answer from context alone. If shell is available, it may try to execute a command rather than delegating back to your application logic. Unnecessary tool calls mean unnecessary latency and token consumption.

By being explicit about which built-ins are in scope, you make the model's decision surface smaller and more predictable. In a domain-specific agent where you have already provided application tools for domain operations, removing irrelevant built-ins often produces faster, more focused responses.

The OverridesBuiltInTool flag

There is one more thing worth noting in the screenshot that is easy to miss:

CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }),

OverridesBuiltInTool = true signals that your custom tool should take precedence over any built-in with the same name or function. The typical use case is when you want to intercept what would otherwise be a built-in operation and route it through your own logic — for example, providing a custom ReportIntent that logs to your observability stack or applies your own business rules before the result is returned.

Without this flag, if there is a name collision between your tool and a built-in, the behavior is undefined. With it, your implementation wins explicitly.

Combining it all: a practical pattern

Here is a pattern that works well for production SDK applications:

// Only give the model what it actually needs
var tools = new ToolSet()
    .AddCustom("*")              // all my application tools
    .AddBuiltIn("web_fetch");    // only the built-in I've explicitly decided to allow

await using var session = await client.CreateSessionAsync(new()
{
    Model = "gpt-5.5",
    SystemMessage = systemMessageConfig,
    // Approve everything that makes it through — the ToolSet already did the filtering
    OnPermissionRequest = PermissionHandler.ApproveAll,
    AvailableTools = tools,
    Tools =
    [
        CopilotTool.DefineTool(SetCurrentPhase),
        CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }),
        CopilotTool.DefineTool(database.SearchProperties),
    ],
});

Notice the combination of ToolSet filtering with PermissionHandler.ApproveAll. The permission handler approves automatically, but only for tools that survived the ToolSet filter. The filtering is your security boundary; the auto-approval is just quality-of-life for the session. This is the right way to think about it: decide what should be possible at configuration time, then get out of the model's way at runtime.

What about when you want all built-ins?

If you genuinely want all built-in tools available — for example in a general-purpose agent where the model should be able to do anything the CLI can do — just do not set AvailableTools at all. The default behavior includes the full built-in set with per-tool permission prompting.

If you want all built-ins but with auto-approval, that is what the combination of omitting AvailableTools and using PermissionHandler.ApproveAll gives you. The key insight is that setting AvailableTools = new ToolSet() and not calling any Add methods would give you an agent with no tools at all — not the full set. Explicit opt-in is the design.

More information

GitHub Copilot CLI command reference - GitHub Docs

Build your first Copilot-powered app - GitHub Docs

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’: ...