Skip to content

Commit 5724537

Browse files
committed
docs: add Tool Permission Callbacks section to README
Add documentation for the can_use_tool callback mechanism that allows programmatic control over which tools Claude can use at runtime. This addresses issue #137 by making the feature more discoverable. Includes: - Basic example showing allow/deny patterns - Example of modifying tool inputs before execution - Comparison table: can_use_tool vs PreToolUse hooks - Reference to the comprehensive example file
1 parent 27575ae commit 5724537

File tree

1 file changed

+94
-0
lines changed

1 file changed

+94
-0
lines changed

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,100 @@ async with ClaudeSDKClient(options=options) as client:
233233
print(msg)
234234
```
235235

236+
### Tool Permission Callbacks
237+
238+
A **tool permission callback** (`can_use_tool`) lets you programmatically control which tools Claude can use at runtime. Unlike hooks which run after permission is granted, `can_use_tool` intercepts tool requests _before_ execution, allowing you to:
239+
240+
- Allow or deny specific tools based on their inputs
241+
- Modify tool inputs before execution (e.g., redirect file paths)
242+
- Implement custom security policies
243+
- Log tool usage for auditing
244+
245+
**Important:** Without a `can_use_tool` callback, the SDK assumes "Denied" by default for operations outside the allowed working directory.
246+
247+
#### Basic Example
248+
249+
```python
250+
from claude_agent_sdk import (
251+
ClaudeAgentOptions,
252+
ClaudeSDKClient,
253+
PermissionResultAllow,
254+
PermissionResultDeny,
255+
ToolPermissionContext,
256+
)
257+
258+
async def my_permission_callback(
259+
tool_name: str,
260+
input_data: dict,
261+
context: ToolPermissionContext
262+
) -> PermissionResultAllow | PermissionResultDeny:
263+
"""Control tool permissions based on tool type and input."""
264+
265+
# Always allow read-only operations
266+
if tool_name in ["Read", "Glob", "Grep"]:
267+
return PermissionResultAllow()
268+
269+
# Deny writes to system directories
270+
if tool_name in ["Write", "Edit"]:
271+
file_path = input_data.get("file_path", "")
272+
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
273+
return PermissionResultDeny(
274+
message=f"Cannot write to system directory: {file_path}"
275+
)
276+
277+
# Block dangerous bash commands
278+
if tool_name == "Bash":
279+
command = input_data.get("command", "")
280+
if "rm -rf" in command or "sudo" in command:
281+
return PermissionResultDeny(
282+
message="Dangerous command pattern detected"
283+
)
284+
285+
# Allow everything else
286+
return PermissionResultAllow()
287+
288+
# Use the callback
289+
options = ClaudeAgentOptions(
290+
can_use_tool=my_permission_callback,
291+
permission_mode="default"
292+
)
293+
294+
async with ClaudeSDKClient(options) as client:
295+
await client.query("List files in /var/log")
296+
async for msg in client.receive_response():
297+
print(msg)
298+
```
299+
300+
#### Modifying Tool Inputs
301+
302+
You can also modify tool inputs before execution:
303+
304+
```python
305+
async def redirect_writes(
306+
tool_name: str,
307+
input_data: dict,
308+
context: ToolPermissionContext
309+
) -> PermissionResultAllow | PermissionResultDeny:
310+
if tool_name == "Write":
311+
# Redirect all writes to a safe directory
312+
modified_input = input_data.copy()
313+
original_path = input_data.get("file_path", "")
314+
modified_input["file_path"] = f"./safe_output/{original_path.split('/')[-1]}"
315+
return PermissionResultAllow(updated_input=modified_input)
316+
return PermissionResultAllow()
317+
```
318+
319+
#### When to Use Tool Permission Callbacks vs Hooks
320+
321+
| Feature | `can_use_tool` Callback | `PreToolUse` Hook |
322+
|---------|-------------------------|-------------------|
323+
| **Timing** | Before permission is granted | After tool is approved |
324+
| **Can deny execution** | Yes | Yes (via `permissionDecision`) |
325+
| **Can modify inputs** | Yes (`updated_input`) | No |
326+
| **Use case** | Security policies, input sanitization | Logging, validation, side effects |
327+
328+
For a complete example with logging and interactive prompts, see [examples/tool_permission_callback.py](examples/tool_permission_callback.py).
329+
236330
## Types
237331

238332
See [src/claude_agent_sdk/types.py](src/claude_agent_sdk/types.py) for complete type definitions:

0 commit comments

Comments
 (0)