github/copilot-sdk · error
invalid tool name : tool names must match…
Error message
invalid %s tool name %q: tool names must match /^[a-zA-Z0-9_-]+$/ or be the wildcard %q
What it means
validateToolName enforces that tool names either match /^[a-zA-Z0-9_-]+$/ or are exactly the wildcard "*". Names with spaces, dots, slashes, or other characters cannot be resolved by the tool-filter machinery, so AddBuiltIn/AddCustom/AddMCP panic with the offending name quoted. The message explicitly documents the accepted format.
Solutions
- Sanitize the name to the allowed charset (replace dots/colons/slashes with '-' or '_') before adding.
- Strip provider prefixes/qualifiers so only the bare tool identifier is passed.
- Trim whitespace and validate with a regex check in your own code before calling the Add method.
- Use "*" if you actually intend to allow all tools.
Example fix
// before
ts.AddMCP("mcp:filesystem.read") // ':' invalid
// after
name := strings.NewReplacer(":", "_", ".", "_").Replace("mcp:filesystem.read")
ts.AddMCP(name) // "mcp_filesystem_read" Defensive patterns
Strategy: validation
Validate before calling
var toolNameRe = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
func addableToolName(n string) bool { return n == "*" || toolNameRe.MatchString(n) } Type guard
func isValidToolName(n string) bool {
return n == "*" || regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(n)
} Try / catch
defer func() {
if r := recover(); r != nil {
if s, ok := r.(string); ok && strings.Contains(s, "tool names must match") {
log.Fatalf("invalid tool name: %s", s)
}
panic(r)
}
}() Prevention
- Run the same regex the library uses on names before calling Add*.
- Strip provider prefixes and normalize separators (dots/colons to '_') when ingesting external tool listings.
- Trim whitespace from names parsed from CSV/config inputs.
When it happens
Trigger: Calling AddBuiltIn("my.tool"), AddCustom("read files"), AddMCP("tools/*"), or any name containing characters outside [a-zA-Z0-9_-]; also full provider-qualified names like "mcp:fs.read" that include a colon.
Common situations: Copying tool identifiers from server listings that use dotted/colon-qualified names; including a path or extension in the tool name; whitespace from CSV parsing (" read ").
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- BuiltinPluginDirectories must contain only absolute paths
- URIConnection requires a non-empty URL
- err.Error()
- invalid tool name: must not be empty
- Invalid tool name ' ': tool names must match…
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/9531608399a3d464.
Report an issue: GitHub.
Appendix: source
Thrown at go/toolset.go:95
return s
}
// ToSlice returns a defensive copy of the accumulated filter strings.
func (s *ToolSet) ToSlice() []string {
out := make([]string, len(s.items))
copy(out, s.items)
return out
}
func validateToolName(kind, name string) {
if name == "" {
panic(fmt.Sprintf("invalid %s tool name: must not be empty", kind))
}
if name == "*" {
return
}
if !toolNameRegex.MatchString(name) {
panic(fmt.Sprintf(
"invalid %s tool name %q: tool names must match /^[a-zA-Z0-9_-]+$/ or be the wildcard %q",
kind, name, "*"))
}
}
// BuiltInToolsIsolated lists built-in tools that operate only within the
// bounds of a single session — no host filesystem access outside the session,
// no cross-session state, no host environment access, no network. Safe to
// enable in [ModeEmpty] scenarios (e.g. multi-tenant servers) without leaking
// host capabilities.
//
// Contract: tools in this set MUST NOT be extended (even behind options or
// args) to read or write state outside the session boundary. Adding
// cross-session or host-state behavior to one of these tools is a breaking
// change that requires removing it from this set.
var BuiltInToolsIsolated = []string{
"ask_user",
"task_complete",View on GitHub (pinned to cd8cf15dc3)