Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/claude_agent_sdk/_internal/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ async def _handle_control_request(self, request: SDKControlRequest) -> None:
or [],
tool_use_id=permission_request.get("tool_use_id"),
agent_id=permission_request.get("agent_id"),
blocked_path=permission_request.get("blocked_path"),
)

response = await self.can_use_tool(
Expand Down
5 changes: 5 additions & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ class ToolPermissionContext:
Multiple tool calls in the same assistant message will have different tool_use_ids."""
agent_id: str | None = None
"""If running within the context of a sub-agent, the sub-agent's ID."""
blocked_path: str | None = None
"""Filesystem path that triggered this permission request, when the tool was
blocked by an `additionalDirectories`/`allowedDirectories` constraint or a
path-scoped permission rule. None when the request is not path-scoped (e.g.
the tool itself is restricted regardless of inputs)."""


# Match TypeScript's PermissionResult structure
Expand Down
75 changes: 75 additions & 0 deletions tests/test_tool_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,81 @@ async def capture_callback(
assert received_context.tool_use_id == "toolu_01XYZ789"
assert received_context.agent_id is None

@pytest.mark.asyncio
async def test_permission_callback_receives_blocked_path(self):
"""Test that blocked_path is forwarded to the context when set."""
received_context = None

async def capture_callback(
tool_name: str, input_data: dict, context: ToolPermissionContext
) -> PermissionResultAllow:
nonlocal received_context
received_context = context
return PermissionResultAllow()

transport = MockTransport()
query = Query(
transport=transport,
is_streaming_mode=True,
can_use_tool=capture_callback,
hooks=None,
)

request = {
"type": "control_request",
"request_id": "test-blockedpath",
"request": {
"subtype": "can_use_tool",
"tool_name": "Read",
"input": {"file_path": "/etc/passwd"},
"permission_suggestions": [],
"tool_use_id": "toolu_01PATH",
"blocked_path": "/etc/passwd",
},
}

await query._handle_control_request(request)

assert received_context is not None
assert received_context.blocked_path == "/etc/passwd"

@pytest.mark.asyncio
async def test_permission_callback_missing_blocked_path(self):
"""Test that blocked_path defaults to None when not sent."""
received_context = None

async def capture_callback(
tool_name: str, input_data: dict, context: ToolPermissionContext
) -> PermissionResultAllow:
nonlocal received_context
received_context = context
return PermissionResultAllow()

transport = MockTransport()
query = Query(
transport=transport,
is_streaming_mode=True,
can_use_tool=capture_callback,
hooks=None,
)

request = {
"type": "control_request",
"request_id": "test-nopath",
"request": {
"subtype": "can_use_tool",
"tool_name": "TestTool",
"input": {},
"permission_suggestions": [],
"tool_use_id": "toolu_01NOPATH",
},
}

await query._handle_control_request(request)

assert received_context is not None
assert received_context.blocked_path is None

@pytest.mark.asyncio
async def test_callback_exception_handling(self):
"""Test that callback exceptions are properly handled."""
Expand Down