github/copilot-sdk · error · TypeError
tool filter must be a ToolSet or list[str], not str. Pass a…
Error message
tool filter must be a ToolSet or list[str], not str. Pass a single-element list (e.g. ["builtin:bash"]) or a ToolSet (e.g. ToolSet().add_builtin('bash')). What it means
The tool filter for create_session/resume_session must be a ToolSet or a list of strings — a bare string is rejected with guidance on the correct forms. This is an explicit anti-footgun: a single string like 'builtin:bash' would otherwise be iterated character-by-character into a meaningless list of filters.
Solutions
- Wrap the string in a list: tools=['builtin:bash']
- Prefer the typed API: ToolSet().add_builtin('bash') passed as the filter
- Update legacy call sites that passed a single tool name string to use the list/ToolSet form
- Coerce defensively in shared helpers: tools = [tools] if isinstance(tools, str) else tools
Example fix
// before
client.create_session(tools="builtin:bash")
// after
client.create_session(tools=["builtin:bash"])
# or
client.create_session(tools=ToolSet().add_builtin("bash")) Defensive patterns
Strategy: type-guard
Validate before calling
if isinstance(tools, str):
raise TypeError("pass a list[str] or ToolSet, not a bare string") Type guard
def is_valid_tool_filter(value) -> bool:
return value is None or isinstance(value, (ToolSet, list)) Try / catch
try:
session = client.create_session(tools=tools)
except TypeError as e:
if "not str" in str(e):
session = client.create_session(tools=[tools]) Prevention
- Always build filters with ToolSet rather than raw strings/lists
- Wrap single tool names in a list immediately: [name]
- Enable type checking so str passed where list[str] is expected is flagged
- Centralize session creation in one helper that normalizes the filter argument
When it happens
Trigger: Calling create_session(tools='builtin:bash') or resume_session(available_tools="mcp:*") — passing a str instead of ['builtin:bash'] or ToolSet().add_builtin('bash').
Common situations: Developers pass a single tool name directly because one tool is all they need, or copy an example that used a list but pass a string built from a variable; older code that predates the ToolSet API may still pass strings.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- invalid tool name: must not be empty
- invalid tool name : tool names must match…
- invalid entry '*': there is no bare wildcard. Use…
- CopilotClient is in mode='empty' but create_session was…
- name is required when using define_tool with handler=
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/cae88f7fb4faabe3.
Report an issue: GitHub.
Appendix: source
Thrown at python/copilot/_mode.py:121
"list_agents",
"send_inbox",
"context_board",
"skill",
]
def _normalize_tool_filter(value: Any) -> list[str] | None:
"""Accept ``ToolSet``, ``list[str]``, or ``None``; return a list or ``None``.
Reject plain ``str`` explicitly — ``list("foo")`` would silently shred it
into characters, sending an invalid tool filter list on the wire.
"""
if value is None:
return None
if isinstance(value, ToolSet):
return value.to_list()
if isinstance(value, str):
raise TypeError(
"tool filter must be a ToolSet or list[str], not str. "
'Pass a single-element list (e.g. ["builtin:bash"]) or a '
"ToolSet (e.g. ToolSet().add_builtin('bash'))."
)
return list(value)
def _validate_tool_filter_list(field: str, items: list[str] | None) -> None:
"""Reject bare ``"*"`` entries (must use ``builtin:*``/``mcp:*``/``custom:*``)."""
if items is None:
return
for entry in items:
if entry == "*":
raise ValueError(
f"invalid {field} entry '*': there is no bare wildcard. "
"Use ToolSet().add_builtin('*'), .add_mcp('*'), or "
".add_custom('*') to target a specific source."
)View on GitHub (pinned to cd8cf15dc3)