Skip to content

Commit ea0ef25

Browse files
ashwin-antclaude
andauthored
feat: add tools option to ClaudeAgentOptions (#389)
Add support for the `tools` option matching the TypeScript SDK, which controls the base set of available tools separately from allowed/disallowed tool filtering. Supports three modes: - Array of tool names: `["Read", "Edit", "Bash"]` - Empty array: `[]` (disables all built-in tools) - Preset object: `{"type": "preset", "preset": "claude_code"}` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <[email protected]>
1 parent 4e56cb1 commit ea0ef25

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

examples/tools_option.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
"""Example demonstrating the tools option and verifying tools in system message."""
3+
4+
import anyio
5+
6+
from claude_agent_sdk import (
7+
AssistantMessage,
8+
ClaudeAgentOptions,
9+
ResultMessage,
10+
SystemMessage,
11+
TextBlock,
12+
query,
13+
)
14+
15+
16+
async def tools_array_example():
17+
"""Example with tools as array of specific tool names."""
18+
print("=== Tools Array Example ===")
19+
print("Setting tools=['Read', 'Glob', 'Grep']")
20+
print()
21+
22+
options = ClaudeAgentOptions(
23+
tools=["Read", "Glob", "Grep"],
24+
max_turns=1,
25+
)
26+
27+
async for message in query(
28+
prompt="What tools do you have available? Just list them briefly.",
29+
options=options,
30+
):
31+
if isinstance(message, SystemMessage) and message.subtype == "init":
32+
tools = message.data.get("tools", [])
33+
print(f"Tools from system message: {tools}")
34+
print()
35+
elif isinstance(message, AssistantMessage):
36+
for block in message.content:
37+
if isinstance(block, TextBlock):
38+
print(f"Claude: {block.text}")
39+
elif isinstance(message, ResultMessage):
40+
if message.total_cost_usd:
41+
print(f"\nCost: ${message.total_cost_usd:.4f}")
42+
print()
43+
44+
45+
async def tools_empty_array_example():
46+
"""Example with tools as empty array (disables all built-in tools)."""
47+
print("=== Tools Empty Array Example ===")
48+
print("Setting tools=[] (disables all built-in tools)")
49+
print()
50+
51+
options = ClaudeAgentOptions(
52+
tools=[],
53+
max_turns=1,
54+
)
55+
56+
async for message in query(
57+
prompt="What tools do you have available? Just list them briefly.",
58+
options=options,
59+
):
60+
if isinstance(message, SystemMessage) and message.subtype == "init":
61+
tools = message.data.get("tools", [])
62+
print(f"Tools from system message: {tools}")
63+
print()
64+
elif isinstance(message, AssistantMessage):
65+
for block in message.content:
66+
if isinstance(block, TextBlock):
67+
print(f"Claude: {block.text}")
68+
elif isinstance(message, ResultMessage):
69+
if message.total_cost_usd:
70+
print(f"\nCost: ${message.total_cost_usd:.4f}")
71+
print()
72+
73+
74+
async def tools_preset_example():
75+
"""Example with tools preset (all default Claude Code tools)."""
76+
print("=== Tools Preset Example ===")
77+
print("Setting tools={'type': 'preset', 'preset': 'claude_code'}")
78+
print()
79+
80+
options = ClaudeAgentOptions(
81+
tools={"type": "preset", "preset": "claude_code"},
82+
max_turns=1,
83+
)
84+
85+
async for message in query(
86+
prompt="What tools do you have available? Just list them briefly.",
87+
options=options,
88+
):
89+
if isinstance(message, SystemMessage) and message.subtype == "init":
90+
tools = message.data.get("tools", [])
91+
print(f"Tools from system message ({len(tools)} tools): {tools[:5]}...")
92+
print()
93+
elif isinstance(message, AssistantMessage):
94+
for block in message.content:
95+
if isinstance(block, TextBlock):
96+
print(f"Claude: {block.text}")
97+
elif isinstance(message, ResultMessage):
98+
if message.total_cost_usd:
99+
print(f"\nCost: ${message.total_cost_usd:.4f}")
100+
print()
101+
102+
103+
async def main():
104+
"""Run all examples."""
105+
await tools_array_example()
106+
await tools_empty_array_example()
107+
await tools_preset_example()
108+
109+
110+
if __name__ == "__main__":
111+
anyio.run(main)

src/claude_agent_sdk/_internal/transport/subprocess_cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ def _build_command(self) -> list[str]:
185185
["--append-system-prompt", self._options.system_prompt["append"]]
186186
)
187187

188+
# Handle tools option (base set of tools)
189+
if self._options.tools is not None:
190+
tools = self._options.tools
191+
if isinstance(tools, list):
192+
if len(tools) == 0:
193+
cmd.extend(["--tools", ""])
194+
else:
195+
cmd.extend(["--tools", ",".join(tools)])
196+
else:
197+
# Preset object - 'claude_code' preset maps to 'default'
198+
cmd.extend(["--tools", "default"])
199+
188200
if self._options.allowed_tools:
189201
cmd.extend(["--allowedTools", ",".join(self._options.allowed_tools)])
190202

src/claude_agent_sdk/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class SystemPromptPreset(TypedDict):
2929
append: NotRequired[str]
3030

3131

32+
class ToolsPreset(TypedDict):
33+
"""Tools preset configuration."""
34+
35+
type: Literal["preset"]
36+
preset: Literal["claude_code"]
37+
38+
3239
@dataclass
3340
class AgentDefinition:
3441
"""Agent definition configuration."""
@@ -606,6 +613,7 @@ class StreamEvent:
606613
class ClaudeAgentOptions:
607614
"""Query options for Claude SDK."""
608615

616+
tools: list[str] | ToolsPreset | None = None
609617
allowed_tools: list[str] = field(default_factory=list)
610618
system_prompt: str | SystemPromptPreset | None = None
611619
mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict)

tests/test_transport.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,3 +647,49 @@ def test_sandbox_network_config(self):
647647
assert network["allowLocalBinding"] is True
648648
assert network["httpProxyPort"] == 8080
649649
assert network["socksProxyPort"] == 8081
650+
651+
def test_build_command_with_tools_array(self):
652+
"""Test building CLI command with tools as array of tool names."""
653+
transport = SubprocessCLITransport(
654+
prompt="test",
655+
options=make_options(tools=["Read", "Edit", "Bash"]),
656+
)
657+
658+
cmd = transport._build_command()
659+
assert "--tools" in cmd
660+
tools_idx = cmd.index("--tools")
661+
assert cmd[tools_idx + 1] == "Read,Edit,Bash"
662+
663+
def test_build_command_with_tools_empty_array(self):
664+
"""Test building CLI command with tools as empty array (disables all tools)."""
665+
transport = SubprocessCLITransport(
666+
prompt="test",
667+
options=make_options(tools=[]),
668+
)
669+
670+
cmd = transport._build_command()
671+
assert "--tools" in cmd
672+
tools_idx = cmd.index("--tools")
673+
assert cmd[tools_idx + 1] == ""
674+
675+
def test_build_command_with_tools_preset(self):
676+
"""Test building CLI command with tools preset."""
677+
transport = SubprocessCLITransport(
678+
prompt="test",
679+
options=make_options(tools={"type": "preset", "preset": "claude_code"}),
680+
)
681+
682+
cmd = transport._build_command()
683+
assert "--tools" in cmd
684+
tools_idx = cmd.index("--tools")
685+
assert cmd[tools_idx + 1] == "default"
686+
687+
def test_build_command_without_tools(self):
688+
"""Test building CLI command without tools option (default None)."""
689+
transport = SubprocessCLITransport(
690+
prompt="test",
691+
options=make_options(),
692+
)
693+
694+
cmd = transport._build_command()
695+
assert "--tools" not in cmd

0 commit comments

Comments
 (0)