From 3024467c13a059089a4e5cbfb1d3eea663c89f84 Mon Sep 17 00:00:00 2001 From: joe shamon Date: Thu, 9 Apr 2026 14:00:43 -0700 Subject: [PATCH 1/5] feat(types): add top-level skills option to ClaudeAgentOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `skills: list[str] | None` field to ClaudeAgentOptions that mirrors the existing field on AgentDefinition. When set, the SDK automatically: - Adds `Skill` (or `Skill(name)` patterns for specific names) to the `--allowedTools` CLI flag. - Defaults `setting_sources` to `["user", "project"]` when not already configured, so installed SKILL.md files are discovered. Previously, enabling skills required both "Skill" in allowed_tools and an explicit setting_sources list — a footgun the SDK can easily remove. The existing `allowed_tools` and `setting_sources` fields are unchanged and still take precedence when the caller sets them explicitly. The options object itself is never mutated. --- .../_internal/transport/subprocess_cli.py | 50 +++++++++++- src/claude_agent_sdk/types.py | 10 +++ tests/test_transport.py | 78 +++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 4b47f115..1b592c92 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -162,6 +162,44 @@ def _build_settings_value(self) -> str | None: return json.dumps(settings_obj) + def _apply_skills_defaults( + self, + ) -> tuple[list[str], list[str] | None]: + """Compute effective allowed_tools and setting_sources for skills. + + If ``options.skills`` is not None, injects the ``Skill`` tool (or + ``Skill(name)`` entries) into allowed_tools and defaults + setting_sources to ``["user", "project"]`` when unset, so that the + CLI discovers installed skills without the caller having to wire up + both options manually. + + Does not mutate the original options object. + """ + allowed_tools: list[str] = list(self._options.allowed_tools) + setting_sources: list[str] | None = ( + list(self._options.setting_sources) + if self._options.setting_sources is not None + else None + ) + + skills = self._options.skills + if skills is None: + return allowed_tools, setting_sources + + if not skills: + if "Skill" not in allowed_tools: + allowed_tools.append("Skill") + else: + for name in skills: + pattern = f"Skill({name})" + if pattern not in allowed_tools: + allowed_tools.append(pattern) + + if setting_sources is None: + setting_sources = ["user", "project"] + + return allowed_tools, setting_sources + def _build_command(self) -> list[str]: """Build CLI command with arguments.""" if self._cli_path is None: @@ -193,8 +231,12 @@ def _build_command(self) -> list[str]: # Preset object - 'claude_code' preset maps to 'default' cmd.extend(["--tools", "default"]) - if self._options.allowed_tools: - cmd.extend(["--allowedTools", ",".join(self._options.allowed_tools)]) + effective_allowed_tools, effective_setting_sources = ( + self._apply_skills_defaults() + ) + + if effective_allowed_tools: + cmd.extend(["--allowedTools", ",".join(effective_allowed_tools)]) if self._options.max_turns: cmd.extend(["--max-turns", str(self._options.max_turns)]) @@ -280,8 +322,8 @@ def _build_command(self) -> list[str]: # Agents are always sent via initialize request (matching TypeScript SDK) # No --agents CLI flag needed - if self._options.setting_sources: - cmd.extend(["--setting-sources", ",".join(self._options.setting_sources)]) + if effective_setting_sources: + cmd.extend(["--setting-sources", ",".join(effective_setting_sources)]) # Add plugin directories if self._options.plugins: diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index a82a8b9b..91983e91 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -1222,6 +1222,16 @@ class ClaudeAgentOptions: agents: dict[str, AgentDefinition] | None = None # Setting sources to load (user, project, local) setting_sources: list[SettingSource] | None = None + # Skills to enable for the main session. When set, the SDK automatically + # enables the ``Skill`` tool and defaults ``setting_sources`` to + # ``["user", "project"]`` (if not already set) so installed SKILL.md + # files are discovered. + # * ``None`` (default): no automatic skill configuration — manage + # ``allowed_tools`` and ``setting_sources`` directly. + # * ``[]``: enable all discovered skills. + # * ``[name, ...]``: enable only the listed skills (added as + # ``Skill(name)`` entries in ``--allowedTools``). + skills: list[str] | None = None # Sandbox configuration for bash command isolation. # Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch), # not from these sandbox settings. diff --git a/tests/test_transport.py b/tests/test_transport.py index b2c40923..aec6a971 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -456,6 +456,84 @@ def test_build_command_setting_sources_included_when_provided(self): idx = cmd.index("--setting-sources") assert cmd[idx + 1] == "user,project" + def test_build_command_skills_none_leaves_options_untouched(self): + """When skills is None (default), neither allowed_tools nor setting_sources change.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options(), + ) + cmd = transport._build_command() + assert "--allowedTools" not in cmd + assert "--setting-sources" not in cmd + + def test_build_command_skills_empty_list_enables_skill_tool(self): + """Empty skills list enables the bare Skill tool and defaults setting_sources.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options(skills=[]), + ) + cmd = transport._build_command() + assert "--allowedTools" in cmd + assert cmd[cmd.index("--allowedTools") + 1] == "Skill" + assert "--setting-sources" in cmd + assert cmd[cmd.index("--setting-sources") + 1] == "user,project" + + def test_build_command_skills_named_list_uses_skill_patterns(self): + """Non-empty skills list adds Skill(name) entries and defaults setting_sources.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options(skills=["pdf", "docx"]), + ) + cmd = transport._build_command() + assert "--allowedTools" in cmd + assert cmd[cmd.index("--allowedTools") + 1] == "Skill(pdf),Skill(docx)" + assert "--setting-sources" in cmd + assert cmd[cmd.index("--setting-sources") + 1] == "user,project" + + def test_build_command_skills_merges_with_existing_allowed_tools(self): + """skills augment (not replace) an existing allowed_tools list.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options( + allowed_tools=["Read", "Write"], + skills=["pdf"], + ), + ) + cmd = transport._build_command() + assert cmd[cmd.index("--allowedTools") + 1] == "Read,Write,Skill(pdf)" + + def test_build_command_skills_preserves_user_setting_sources(self): + """When setting_sources is explicitly provided, skills should not override it.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options( + skills=[], + setting_sources=["local"], + ), + ) + cmd = transport._build_command() + assert cmd[cmd.index("--setting-sources") + 1] == "local" + + def test_build_command_skills_does_not_mutate_options(self): + """Applying skills defaults must not mutate the caller's options object.""" + options = make_options(allowed_tools=["Read"], skills=["pdf"]) + transport = SubprocessCLITransport(prompt="test", options=options) + transport._build_command() + assert options.allowed_tools == ["Read"] + assert options.setting_sources is None + + def test_build_command_skills_does_not_duplicate_entries(self): + """Injecting Skill entries is idempotent when caller already listed them.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options( + allowed_tools=["Skill(pdf)"], + skills=["pdf"], + ), + ) + cmd = transport._build_command() + assert cmd[cmd.index("--allowedTools") + 1] == "Skill(pdf)" + def test_build_command_with_extra_args(self): """Test building CLI command with extra_args for future flags.""" transport = SubprocessCLITransport( From c8dd3f1ed24bbe2126c5c4eb23d0e1fe09218a80 Mon Sep 17 00:00:00 2001 From: joe shamon Date: Thu, 9 Apr 2026 14:32:44 -0700 Subject: [PATCH 2/5] feat: send skills on initialize request for CLI-side load filtering In addition to translating `skills` into `Skill(name)` allowedTools entries, also forward the list on the SDK `initialize` control request. A supporting CLI can use this to filter which skills are loaded into the system prompt (not just permission-gated). Older CLIs ignore unknown initialize fields, so this is forward-compatible. --- src/claude_agent_sdk/_internal/client.py | 1 + src/claude_agent_sdk/_internal/query.py | 6 ++++++ src/claude_agent_sdk/client.py | 1 + src/claude_agent_sdk/types.py | 4 +++- tests/test_query.py | 15 +++++++++++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/claude_agent_sdk/_internal/client.py b/src/claude_agent_sdk/_internal/client.py index 76323323..d6b787f4 100644 --- a/src/claude_agent_sdk/_internal/client.py +++ b/src/claude_agent_sdk/_internal/client.py @@ -128,6 +128,7 @@ async def process_query( initialize_timeout=initialize_timeout, agents=agents_dict, exclude_dynamic_sections=exclude_dynamic_sections, + skills=configured_options.skills, ) try: diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 80b6d93c..86c75d87 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -77,6 +77,7 @@ def __init__( initialize_timeout: float = 60.0, agents: dict[str, dict[str, Any]] | None = None, exclude_dynamic_sections: bool | None = None, + skills: list[str] | None = None, ): """Initialize Query with transport and callbacks. @@ -90,6 +91,8 @@ def __init__( agents: Optional agent definitions to send via initialize exclude_dynamic_sections: Optional preset-prompt flag to send via initialize (see ``SystemPromptPreset``) + skills: Optional skill allowlist to send via initialize so the CLI + can filter which skills are loaded into the system prompt """ self._initialize_timeout = initialize_timeout self.transport = transport @@ -99,6 +102,7 @@ def __init__( self.sdk_mcp_servers = sdk_mcp_servers or {} self._agents = agents self._exclude_dynamic_sections = exclude_dynamic_sections + self._skills = skills # Control protocol state self.pending_control_responses: dict[str, anyio.Event] = {} @@ -160,6 +164,8 @@ async def initialize(self) -> dict[str, Any] | None: request["agents"] = self._agents if self._exclude_dynamic_sections is not None: request["excludeDynamicSections"] = self._exclude_dynamic_sections + if self._skills is not None: + request["skills"] = self._skills # Use longer timeout for initialize since MCP servers may take time to start response = await self._send_control_request( diff --git a/src/claude_agent_sdk/client.py b/src/claude_agent_sdk/client.py index 64f845fb..7d7911ac 100644 --- a/src/claude_agent_sdk/client.py +++ b/src/claude_agent_sdk/client.py @@ -186,6 +186,7 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: initialize_timeout=initialize_timeout, agents=agents_dict, exclude_dynamic_sections=exclude_dynamic_sections, + skills=self.options.skills, ) # Start reading messages and initialize diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 91983e91..38be3949 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -1225,7 +1225,9 @@ class ClaudeAgentOptions: # Skills to enable for the main session. When set, the SDK automatically # enables the ``Skill`` tool and defaults ``setting_sources`` to # ``["user", "project"]`` (if not already set) so installed SKILL.md - # files are discovered. + # files are discovered. The list is also sent on the ``initialize`` control + # request so a supporting CLI can filter which skills are loaded into the + # system prompt (older CLIs ignore the field). # * ``None`` (default): no automatic skill configuration — manage # ``allowed_tools`` and ``setting_sources`` directly. # * ``[]``: enable all discovered skills. diff --git a/tests/test_query.py b/tests/test_query.py index 30e51c09..d5f258e7 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -59,6 +59,21 @@ def test_initialize_omits_exclude_dynamic_sections_when_unset(): assert "excludeDynamicSections" not in sent +def test_initialize_sends_skills_when_set(): + """Query.initialize() includes skills (including empty list) in the control request.""" + sent = _capture_initialize_request(skills=["pdf", "docx"]) + assert sent["skills"] == ["pdf", "docx"] + + sent_empty = _capture_initialize_request(skills=[]) + assert sent_empty["skills"] == [] + + +def test_initialize_omits_skills_when_unset(): + """skills is absent from initialize when not configured.""" + sent = _capture_initialize_request() + assert "skills" not in sent + + def _make_mock_transport(messages, control_requests=None): """Create a mock transport that yields messages and optionally sends control requests. From 7596ad44296f61f43319cb691bd9acb4f9a664e3 Mon Sep 17 00:00:00 2001 From: joe shamon Date: Thu, 9 Apr 2026 20:18:54 -0700 Subject: [PATCH 3/5] docs: clarify skills is a context filter, not a sandbox Unlisted skills are hidden from the listing and blocked at the Skill tool, but their files remain readable via Read/Bash. Document the boundary and the alternatives (local plugin, deny rules) for users who need hard isolation. --- src/claude_agent_sdk/types.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 38be3949..5f02db8a 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -1233,6 +1233,16 @@ class ClaudeAgentOptions: # * ``[]``: enable all discovered skills. # * ``[name, ...]``: enable only the listed skills (added as # ``Skill(name)`` entries in ``--allowedTools``). + # + # .. note:: + # This is a **context filter**, not a sandbox. Unlisted skills are + # hidden from the model's skill listing and cannot be invoked via the + # Skill tool, but their files remain on disk — a session with ``Read`` + # or ``Bash`` can still access ``.claude/skills/**`` directly. For + # hard isolation, either omit those files from the working directory, + # bundle the desired subset as a local plugin (``plugins=[...]`` with + # ``setting_sources=None``), or add explicit permission deny rules. + # Do not store secrets in skill files. skills: list[str] | None = None # Sandbox configuration for bash command isolation. # Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch), From 01f9a4696db2779408e916f7673cd075f3e52d88 Mon Sep 17 00:00:00 2001 From: joe shamon Date: Thu, 9 Apr 2026 20:34:25 -0700 Subject: [PATCH 4/5] refactor: skills accepts 'all' sentinel; [] no longer means everything API design refinement: skills is now the one place to enable skills (users should not put 'Skill' in allowed_tools directly). None - skills off (default) 'all' - every discovered skill [name,...] - named subset only [] - degenerate subset; setting_sources still defaults but no Skill entries are added (natural list semantics) Type widened to list[str] | Literal['all'] | None. Transport, query init, docstring, and tests updated. --- src/claude_agent_sdk/_internal/query.py | 4 ++-- .../_internal/transport/subprocess_cli.py | 12 +++++----- src/claude_agent_sdk/types.py | 24 +++++++++---------- tests/test_query.py | 5 +++- tests/test_transport.py | 19 +++++++++++---- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 86c75d87..0739c910 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -6,7 +6,7 @@ import os from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable from contextlib import suppress -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import anyio from mcp.types import ( @@ -77,7 +77,7 @@ def __init__( initialize_timeout: float = 60.0, agents: dict[str, dict[str, Any]] | None = None, exclude_dynamic_sections: bool | None = None, - skills: list[str] | None = None, + skills: list[str] | Literal["all"] | None = None, ): """Initialize Query with transport and callbacks. diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 1b592c92..6733cb4f 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -167,11 +167,11 @@ def _apply_skills_defaults( ) -> tuple[list[str], list[str] | None]: """Compute effective allowed_tools and setting_sources for skills. - If ``options.skills`` is not None, injects the ``Skill`` tool (or - ``Skill(name)`` entries) into allowed_tools and defaults - setting_sources to ``["user", "project"]`` when unset, so that the - CLI discovers installed skills without the caller having to wire up - both options manually. + When ``options.skills`` is ``"all"``, injects the bare ``Skill`` tool; + when it is a list, injects ``Skill(name)`` for each entry. In either + case ``setting_sources`` defaults to ``["user", "project"]`` when + unset so the CLI discovers installed skills without the caller having + to wire up both options manually. ``None`` is a no-op. Does not mutate the original options object. """ @@ -186,7 +186,7 @@ def _apply_skills_defaults( if skills is None: return allowed_tools, setting_sources - if not skills: + if skills == "all": if "Skill" not in allowed_tools: allowed_tools.append("Skill") else: diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 5f02db8a..75bb9902 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -1222,17 +1222,17 @@ class ClaudeAgentOptions: agents: dict[str, AgentDefinition] | None = None # Setting sources to load (user, project, local) setting_sources: list[SettingSource] | None = None - # Skills to enable for the main session. When set, the SDK automatically - # enables the ``Skill`` tool and defaults ``setting_sources`` to - # ``["user", "project"]`` (if not already set) so installed SKILL.md - # files are discovered. The list is also sent on the ``initialize`` control - # request so a supporting CLI can filter which skills are loaded into the - # system prompt (older CLIs ignore the field). - # * ``None`` (default): no automatic skill configuration — manage - # ``allowed_tools`` and ``setting_sources`` directly. - # * ``[]``: enable all discovered skills. - # * ``[name, ...]``: enable only the listed skills (added as - # ``Skill(name)`` entries in ``--allowedTools``). + # Skills to enable for the main session. This is the one place to turn + # skills on; you do not need to add ``"Skill"`` to ``allowed_tools`` or + # set ``setting_sources`` yourself — the SDK does both when this is set. + # The value is also sent on the ``initialize`` control request so a + # supporting CLI can filter which skills are loaded into the system prompt + # (older CLIs ignore the field). + # * ``None`` (default): skills are off. + # * ``"all"``: enable every discovered skill. + # * ``[name, ...]``: enable only the listed skills. Names match the + # SKILL.md ``name`` / directory name, or ``plugin:skill`` for + # plugin-qualified skills. # # .. note:: # This is a **context filter**, not a sandbox. Unlisted skills are @@ -1243,7 +1243,7 @@ class ClaudeAgentOptions: # bundle the desired subset as a local plugin (``plugins=[...]`` with # ``setting_sources=None``), or add explicit permission deny rules. # Do not store secrets in skill files. - skills: list[str] | None = None + skills: list[str] | Literal["all"] | None = None # Sandbox configuration for bash command isolation. # Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch), # not from these sandbox settings. diff --git a/tests/test_query.py b/tests/test_query.py index d5f258e7..ae3554dd 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -60,10 +60,13 @@ def test_initialize_omits_exclude_dynamic_sections_when_unset(): def test_initialize_sends_skills_when_set(): - """Query.initialize() includes skills (including empty list) in the control request.""" + """Query.initialize() includes skills (list, 'all', or empty) in the control request.""" sent = _capture_initialize_request(skills=["pdf", "docx"]) assert sent["skills"] == ["pdf", "docx"] + sent_all = _capture_initialize_request(skills="all") + assert sent_all["skills"] == "all" + sent_empty = _capture_initialize_request(skills=[]) assert sent_empty["skills"] == [] diff --git a/tests/test_transport.py b/tests/test_transport.py index aec6a971..f3a846e4 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -466,11 +466,11 @@ def test_build_command_skills_none_leaves_options_untouched(self): assert "--allowedTools" not in cmd assert "--setting-sources" not in cmd - def test_build_command_skills_empty_list_enables_skill_tool(self): - """Empty skills list enables the bare Skill tool and defaults setting_sources.""" + def test_build_command_skills_all_enables_skill_tool(self): + """skills='all' enables the bare Skill tool and defaults setting_sources.""" transport = SubprocessCLITransport( prompt="test", - options=make_options(skills=[]), + options=make_options(skills="all"), ) cmd = transport._build_command() assert "--allowedTools" in cmd @@ -478,6 +478,17 @@ def test_build_command_skills_empty_list_enables_skill_tool(self): assert "--setting-sources" in cmd assert cmd[cmd.index("--setting-sources") + 1] == "user,project" + def test_build_command_skills_empty_list_adds_no_skill_entries(self): + """skills=[] is a degenerate subset: setting_sources defaults, no Skill entries.""" + transport = SubprocessCLITransport( + prompt="test", + options=make_options(skills=[]), + ) + cmd = transport._build_command() + assert "--allowedTools" not in cmd + assert "--setting-sources" in cmd + assert cmd[cmd.index("--setting-sources") + 1] == "user,project" + def test_build_command_skills_named_list_uses_skill_patterns(self): """Non-empty skills list adds Skill(name) entries and defaults setting_sources.""" transport = SubprocessCLITransport( @@ -507,7 +518,7 @@ def test_build_command_skills_preserves_user_setting_sources(self): transport = SubprocessCLITransport( prompt="test", options=make_options( - skills=[], + skills="all", setting_sources=["local"], ), ) From 5ed30f1c83c385477920acbe828f5408e82ca832 Mon Sep 17 00:00:00 2001 From: joe shamon Date: Thu, 9 Apr 2026 21:18:13 -0700 Subject: [PATCH 5/5] fix: omit skills from initialize request when 'all' 'all' and omitted both mean 'no filter' at the wire level, so only send the field when it is an explicit list. Keeps the CLI control schema as a plain string[] (which the zod-to-proto pipeline can represent) while the 'all' sentinel remains at the Python API surface for ergonomics. --- src/claude_agent_sdk/_internal/query.py | 4 +++- tests/test_query.py | 16 +++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 0739c910..30ad2257 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -164,7 +164,9 @@ async def initialize(self) -> dict[str, Any] | None: request["agents"] = self._agents if self._exclude_dynamic_sections is not None: request["excludeDynamicSections"] = self._exclude_dynamic_sections - if self._skills is not None: + # 'all' and omitted are equivalent at the wire level (no filter), so + # only send the field when it's an explicit list. + if isinstance(self._skills, list): request["skills"] = self._skills # Use longer timeout for initialize since MCP servers may take time to start diff --git a/tests/test_query.py b/tests/test_query.py index ae3554dd..1fcb4dea 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -59,22 +59,20 @@ def test_initialize_omits_exclude_dynamic_sections_when_unset(): assert "excludeDynamicSections" not in sent -def test_initialize_sends_skills_when_set(): - """Query.initialize() includes skills (list, 'all', or empty) in the control request.""" +def test_initialize_sends_skills_list(): + """Query.initialize() includes skills only when it is a list.""" sent = _capture_initialize_request(skills=["pdf", "docx"]) assert sent["skills"] == ["pdf", "docx"] - sent_all = _capture_initialize_request(skills="all") - assert sent_all["skills"] == "all" - sent_empty = _capture_initialize_request(skills=[]) assert sent_empty["skills"] == [] -def test_initialize_omits_skills_when_unset(): - """skills is absent from initialize when not configured.""" - sent = _capture_initialize_request() - assert "skills" not in sent +def test_initialize_omits_skills_for_none_and_all(): + """'all' and None both omit skills from initialize (no filter at wire level).""" + assert "skills" not in _capture_initialize_request() + assert "skills" not in _capture_initialize_request(skills=None) + assert "skills" not in _capture_initialize_request(skills="all") def _make_mock_transport(messages, control_requests=None):