Tutorials · Claude Agent SDK · examples / python / 10_permissions.pyExample code

Example code · python

10_permissions.py

Runtime approval with can_use_tool. Allows file creation, denies deletion, rewrites paths.

Raw fileDownload all examples (zip)See its output →

Used in Part 2 · Your approval function

"""10: Runtime approval with can_use_tool. Allows file creation, denies deletion, rewrites paths."""

import asyncio

from claude_agent_sdk import (
    ClaudeAgentOptions,
    HookMatcher,
    PermissionResultAllow,
    PermissionResultDeny,
    ResultMessage,
    ToolPermissionContext,
    UserMessage,
    ToolResultBlock,
    query,
)
from demo_util import PROJECT

decisions = []


async def can_use_tool(tool_name: str, input_data: dict, context: ToolPermissionContext):
    """Stands in for a UI prompt. Policy: never delete, sandbox all writes under scratch/."""
    if tool_name == "Bash" and "rm " in input_data.get("command", ""):
        decisions.append(("deny", tool_name, input_data["command"]))
        return PermissionResultDeny(message="User policy: deletion is not allowed. Leave the file in place.")

    if tool_name == "Write":
        path = input_data["file_path"]
        if "/scratch/" not in path:
            new_path = path.rsplit("/", 1)[0] + "/scratch/" + path.rsplit("/", 1)[1]
            decisions.append(("allow+rewrite", tool_name, f"{path} -> {new_path}"))
            return PermissionResultAllow(updated_input={**input_data, "file_path": new_path})

    decisions.append(("allow", tool_name, str(input_data)[:60]))
    return PermissionResultAllow(updated_input=input_data)


# Required workaround: a (no-op) hook keeps the control stream open for can_use_tool
async def noop_hook(input_data, tool_use_id, context):
    return {"continue_": True}


async def prompt_stream():
    yield {"type": "user", "message": {"role": "user",
           "content": "Create a file named notes.txt containing 'hello', then delete it with rm."}}


async def main():
    async for message in query(
        prompt=prompt_stream(),
        options=ClaudeAgentOptions(
            cwd=PROJECT,
            permission_mode="default",
            can_use_tool=can_use_tool,
            hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[noop_hook])]},
            max_turns=6,
        ),
    ):
        if isinstance(message, UserMessage) and isinstance(message.content, list):
            for block in message.content:
                if isinstance(block, ToolResultBlock):
                    print(f"[tool_result]{' ERROR' if block.is_error else ''} {str(block.content)[:160]}")
        elif isinstance(message, ResultMessage):
            print(f"\n[result] {message.subtype}: {message.result}")
            print("[result] permission_denials:", message.permission_denials)

    print("\nDecisions made by can_use_tool:")
    for d in decisions:
        print("  ", d)


asyncio.run(main())

73 lines · Back to the folder