Skip to content

Commit 01f9a46

Browse files
committed
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.
1 parent 7596ad4 commit 01f9a46

5 files changed

Lines changed: 39 additions & 25 deletions

File tree

src/claude_agent_sdk/_internal/query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable
88
from contextlib import suppress
9-
from typing import TYPE_CHECKING, Any
9+
from typing import TYPE_CHECKING, Any, Literal
1010

1111
import anyio
1212
from mcp.types import (
@@ -77,7 +77,7 @@ def __init__(
7777
initialize_timeout: float = 60.0,
7878
agents: dict[str, dict[str, Any]] | None = None,
7979
exclude_dynamic_sections: bool | None = None,
80-
skills: list[str] | None = None,
80+
skills: list[str] | Literal["all"] | None = None,
8181
):
8282
"""Initialize Query with transport and callbacks.
8383

src/claude_agent_sdk/_internal/transport/subprocess_cli.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,11 @@ def _apply_skills_defaults(
167167
) -> tuple[list[str], list[str] | None]:
168168
"""Compute effective allowed_tools and setting_sources for skills.
169169
170-
If ``options.skills`` is not None, injects the ``Skill`` tool (or
171-
``Skill(name)`` entries) into allowed_tools and defaults
172-
setting_sources to ``["user", "project"]`` when unset, so that the
173-
CLI discovers installed skills without the caller having to wire up
174-
both options manually.
170+
When ``options.skills`` is ``"all"``, injects the bare ``Skill`` tool;
171+
when it is a list, injects ``Skill(name)`` for each entry. In either
172+
case ``setting_sources`` defaults to ``["user", "project"]`` when
173+
unset so the CLI discovers installed skills without the caller having
174+
to wire up both options manually. ``None`` is a no-op.
175175
176176
Does not mutate the original options object.
177177
"""
@@ -186,7 +186,7 @@ def _apply_skills_defaults(
186186
if skills is None:
187187
return allowed_tools, setting_sources
188188

189-
if not skills:
189+
if skills == "all":
190190
if "Skill" not in allowed_tools:
191191
allowed_tools.append("Skill")
192192
else:

src/claude_agent_sdk/types.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,17 +1222,17 @@ class ClaudeAgentOptions:
12221222
agents: dict[str, AgentDefinition] | None = None
12231223
# Setting sources to load (user, project, local)
12241224
setting_sources: list[SettingSource] | None = None
1225-
# Skills to enable for the main session. When set, the SDK automatically
1226-
# enables the ``Skill`` tool and defaults ``setting_sources`` to
1227-
# ``["user", "project"]`` (if not already set) so installed SKILL.md
1228-
# files are discovered. The list is also sent on the ``initialize`` control
1229-
# request so a supporting CLI can filter which skills are loaded into the
1230-
# system prompt (older CLIs ignore the field).
1231-
# * ``None`` (default): no automatic skill configuration — manage
1232-
# ``allowed_tools`` and ``setting_sources`` directly.
1233-
# * ``[]``: enable all discovered skills.
1234-
# * ``[name, ...]``: enable only the listed skills (added as
1235-
# ``Skill(name)`` entries in ``--allowedTools``).
1225+
# Skills to enable for the main session. This is the one place to turn
1226+
# skills on; you do not need to add ``"Skill"`` to ``allowed_tools`` or
1227+
# set ``setting_sources`` yourself — the SDK does both when this is set.
1228+
# The value is also sent on the ``initialize`` control request so a
1229+
# supporting CLI can filter which skills are loaded into the system prompt
1230+
# (older CLIs ignore the field).
1231+
# * ``None`` (default): skills are off.
1232+
# * ``"all"``: enable every discovered skill.
1233+
# * ``[name, ...]``: enable only the listed skills. Names match the
1234+
# SKILL.md ``name`` / directory name, or ``plugin:skill`` for
1235+
# plugin-qualified skills.
12361236
#
12371237
# .. note::
12381238
# This is a **context filter**, not a sandbox. Unlisted skills are
@@ -1243,7 +1243,7 @@ class ClaudeAgentOptions:
12431243
# bundle the desired subset as a local plugin (``plugins=[...]`` with
12441244
# ``setting_sources=None``), or add explicit permission deny rules.
12451245
# Do not store secrets in skill files.
1246-
skills: list[str] | None = None
1246+
skills: list[str] | Literal["all"] | None = None
12471247
# Sandbox configuration for bash command isolation.
12481248
# Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch),
12491249
# not from these sandbox settings.

tests/test_query.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ def test_initialize_omits_exclude_dynamic_sections_when_unset():
6060

6161

6262
def test_initialize_sends_skills_when_set():
63-
"""Query.initialize() includes skills (including empty list) in the control request."""
63+
"""Query.initialize() includes skills (list, 'all', or empty) in the control request."""
6464
sent = _capture_initialize_request(skills=["pdf", "docx"])
6565
assert sent["skills"] == ["pdf", "docx"]
6666

67+
sent_all = _capture_initialize_request(skills="all")
68+
assert sent_all["skills"] == "all"
69+
6770
sent_empty = _capture_initialize_request(skills=[])
6871
assert sent_empty["skills"] == []
6972

tests/test_transport.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,18 +466,29 @@ def test_build_command_skills_none_leaves_options_untouched(self):
466466
assert "--allowedTools" not in cmd
467467
assert "--setting-sources" not in cmd
468468

469-
def test_build_command_skills_empty_list_enables_skill_tool(self):
470-
"""Empty skills list enables the bare Skill tool and defaults setting_sources."""
469+
def test_build_command_skills_all_enables_skill_tool(self):
470+
"""skills='all' enables the bare Skill tool and defaults setting_sources."""
471471
transport = SubprocessCLITransport(
472472
prompt="test",
473-
options=make_options(skills=[]),
473+
options=make_options(skills="all"),
474474
)
475475
cmd = transport._build_command()
476476
assert "--allowedTools" in cmd
477477
assert cmd[cmd.index("--allowedTools") + 1] == "Skill"
478478
assert "--setting-sources" in cmd
479479
assert cmd[cmd.index("--setting-sources") + 1] == "user,project"
480480

481+
def test_build_command_skills_empty_list_adds_no_skill_entries(self):
482+
"""skills=[] is a degenerate subset: setting_sources defaults, no Skill entries."""
483+
transport = SubprocessCLITransport(
484+
prompt="test",
485+
options=make_options(skills=[]),
486+
)
487+
cmd = transport._build_command()
488+
assert "--allowedTools" not in cmd
489+
assert "--setting-sources" in cmd
490+
assert cmd[cmd.index("--setting-sources") + 1] == "user,project"
491+
481492
def test_build_command_skills_named_list_uses_skill_patterns(self):
482493
"""Non-empty skills list adds Skill(name) entries and defaults setting_sources."""
483494
transport = SubprocessCLITransport(
@@ -507,7 +518,7 @@ def test_build_command_skills_preserves_user_setting_sources(self):
507518
transport = SubprocessCLITransport(
508519
prompt="test",
509520
options=make_options(
510-
skills=[],
521+
skills="all",
511522
setting_sources=["local"],
512523
),
513524
)

0 commit comments

Comments
 (0)