My agentic slop goes here. Not intended for anyone else!
1#!/usr/bin/env python3
2# /// script
3# requires-python = ">=3.9"
4# dependencies = [
5# "claude-code-sdk",
6# ]
7# ///
8"""
9Permission demo for Claude Code SDK Python.
10Demonstrates how the permission callback system works.
11"""
12
13import asyncio
14import sys
15import logging
16from typing import Any, Dict
17
18from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions
19from claude_code_sdk.types import (
20 PermissionResultAllow,
21 PermissionResultDeny,
22 ToolPermissionContext,
23)
24
25# Set up logging
26logging.basicConfig(
27 level=logging.INFO,
28 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
29)
30logger = logging.getLogger(__name__)
31
32# Track granted permissions
33granted_permissions = set()
34
35
36async def interactive_permission_callback(
37 tool_name: str,
38 tool_input: Dict[str, Any],
39 context: ToolPermissionContext
40) -> PermissionResultAllow | PermissionResultDeny:
41 """Interactive permission callback that asks user for permission."""
42
43 logger.info(f"🔔 Permission callback invoked for tool: {tool_name}")
44 print(f"\n🔐 PERMISSION REQUEST 🔐")
45 print(f"Tool: {tool_name}")
46
47 # Log the full input for debugging
48 logger.info(f"Full input: {tool_input}")
49
50 # Show input details
51 try:
52 if tool_name == "Read":
53 file_path = tool_input.get("file_path", "")
54 print(f"File: {file_path}")
55 elif tool_name == "Bash":
56 command = tool_input.get("command", "")
57 print(f"Command: {command}")
58 elif tool_name in ["Write", "Edit"]:
59 file_path = tool_input.get("file_path", "")
60 print(f"File: {file_path}")
61 elif tool_name == "Glob":
62 pattern = tool_input.get("pattern", "")
63 path = tool_input.get("path", "(current directory)")
64 print(f"Pattern: {pattern}")
65 print(f"Path: {path}")
66 elif tool_name == "Grep":
67 pattern = tool_input.get("pattern", "")
68 path = tool_input.get("path", "(current directory)")
69 print(f"Pattern: {pattern}")
70 print(f"Path: {path}")
71 else:
72 print(f"Input: {tool_input}")
73 except Exception as e:
74 logger.info(f"Failed to parse input details: {e}")
75
76 # Check if already granted
77 if tool_name in granted_permissions:
78 print("→ Auto-approved (previously granted)")
79 logger.info(f"Returning allow result for {tool_name}")
80 return PermissionResultAllow()
81
82 # Ask user
83 response = input("Allow? [y/N/always]: ").lower().strip()
84
85 if response in ["y", "yes"]:
86 print("→ Allowed (this time only)")
87 logger.info(f"User approved {tool_name} for this request only")
88 return PermissionResultAllow()
89 elif response in ["a", "always"]:
90 granted_permissions.add(tool_name)
91 print(f"✅ Permission granted for: {tool_name}")
92 logger.info(f"User granted permanent permission for {tool_name}")
93 return PermissionResultAllow()
94 else:
95 print(f"❌ Permission denied for: {tool_name}")
96 logger.info(f"User denied permission for {tool_name}")
97 return PermissionResultDeny(
98 message=f"User denied access to {tool_name}",
99 interrupt=False
100 )
101
102
103async def run_demo():
104 """Run the permission demo."""
105 print("🚀 Starting Permission Demo")
106 print("==================================")
107 print("This demo starts with NO permissions.")
108 print("Claude will request permissions as needed.\n")
109
110 # Create options with custom permission callback
111 # Test WITHOUT allowed_tools to see if permission requests come through
112 options = ClaudeCodeOptions(
113 model="sonnet",
114 # allowed_tools=["Read", "Write", "Bash", "Edit", "Glob", "Grep"],
115 can_use_tool=interactive_permission_callback,
116 )
117
118 async with ClaudeSDKClient(options=options) as client:
119 # First prompt - Claude will need to request Read permission
120 print("\n📤 Sending first prompt (reading from ../lib)...")
121 messages = []
122 await client.query(
123 "Please read and analyze the source files in the ../lib directory. "
124 "Focus on the main OCaml modules and their purpose. "
125 "What is the overall architecture of this Claude library?"
126 )
127
128 async for msg in client.receive_response():
129 messages.append(msg)
130 if hasattr(msg, 'content'):
131 if isinstance(msg.content, str):
132 print(f"\n📝 Claude says:\n{msg.content}")
133 elif isinstance(msg.content, list):
134 for block in msg.content:
135 if hasattr(block, 'text'):
136 print(f"\n📝 Claude says:\n{block.text}")
137
138 # Show current permissions
139 print("\n📋 Current permission status:")
140 if granted_permissions:
141 print(f"Currently granted permissions: {', '.join(granted_permissions)}")
142 else:
143 print("No permissions granted yet")
144
145 # Second prompt - will need Write permission
146 print("\n📤 Sending second prompt (writing TEST.md)...")
147 await client.query(
148 "Now write a summary of what you learned about the Claude library "
149 "architecture to a file called TEST.md in the current directory. "
150 "Include the main modules, their purposes, and how they work together."
151 )
152
153 async for msg in client.receive_response():
154 if hasattr(msg, 'content'):
155 if isinstance(msg.content, str):
156 print(f"\n📝 Claude says:\n{msg.content}")
157 elif isinstance(msg.content, list):
158 for block in msg.content:
159 if hasattr(block, 'text'):
160 print(f"\n📝 Claude says:\n{block.text}")
161
162 # Show final permissions
163 print("\n📋 Final permission status:")
164 if granted_permissions:
165 print(f"Currently granted permissions: {', '.join(granted_permissions)}")
166 else:
167 print("No permissions granted yet")
168
169 print("\n==================================")
170 print("✨ Demo complete!")
171
172
173async def main():
174 """Main entry point."""
175 try:
176 await run_demo()
177 except KeyboardInterrupt:
178 print("\n\nDemo interrupted by user.")
179 except Exception as e:
180 logger.error(f"Error in demo: {e}", exc_info=True)
181 sys.exit(1)
182
183
184if __name__ == "__main__":
185 asyncio.run(main())