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.