···11-# Claude OCaml SDK Permission System
22-33-The Claude OCaml SDK provides a flexible permission system for controlling tool access when Claude attempts to use various tools like Read, Write, Bash, etc.
44-55-## Overview
66-77-The permission system allows you to:
88-- Control which tools Claude can use
99-- Implement custom permission callbacks
1010-- Discover what permissions an operation needs
1111-- Grant or deny permissions dynamically
1212-1313-## Permission Modes
1414-1515-Claude supports several permission modes:
1616-- `Default` - Standard permission checks
1717-- `Accept_edits` - Automatically accept file edits
1818-- `Plan` - Planning mode with restricted execution
1919-- `Bypass_permissions` - Skip all permission checks
2020-2121-## Using Permission Callbacks
2222-2323-### Basic Callback
2424-2525-```ocaml
2626-let my_permission_callback ~tool_name ~input ~context =
2727- match tool_name with
2828- | "Read" ->
2929- (* Always allow reading *)
3030- Claude.Permissions.Result.allow ()
3131- | "Write" | "Edit" ->
3232- (* Deny write operations *)
3333- Claude.Permissions.Result.deny
3434- ~message:"Write operations are not allowed"
3535- ~interrupt:false
3636- | _ ->
3737- (* Allow everything else *)
3838- Claude.Permissions.Result.allow ()
3939-4040-let options = Claude.Options.create
4141- ~permission_callback:my_permission_callback
4242- () in
4343-```
4444-4545-### Interactive Callback
4646-4747-See `test/simulated_permissions.ml` for an example that prompts the user interactively.
4848-4949-### Discovery Mode
5050-5151-The discovery callback helps you understand what permissions an operation needs:
5252-5353-```ocaml
5454-let client = Claude.Client.discover_permissions client in
5555-(* Run operations *)
5656-let discovered = Claude.Client.get_discovered_permissions client in
5757-List.iter (fun rule ->
5858- Printf.printf "Tool needed: %s\n"
5959- (Claude.Permissions.Rule.tool_name rule)
6060-) discovered
6161-```
6262-6363-## Permission Results
6464-6565-Callbacks return permission results that can:
6666-- Allow the operation (optionally with modified input)
6767-- Deny the operation (with a message and optional interrupt)
6868-6969-```ocaml
7070-(* Allow with modified input *)
7171-Claude.Permissions.Result.allow
7272- ~updated_input:(Ezjsonm.dict ["sanitized", Ezjsonm.string "data"])
7373- ()
7474-7575-(* Deny and stop execution *)
7676-Claude.Permissions.Result.deny
7777- ~message:"Security policy violation"
7878- ~interrupt:true
7979-```
8080-8181-## Tool Restrictions
8282-8383-You can also restrict tools at the options level:
8484-8585-```ocaml
8686-let options = Claude.Options.create
8787- ~allowed_tools:["Read"; "Grep"] (* Only these tools *)
8888- ~disallowed_tools:["Bash"; "Write"] (* Block these *)
8989- ()
9090-```
9191-9292-## Examples
9393-9494-The `test/` directory contains several examples:
9595-- `simulated_permissions.ml` - Interactive permission demo
9696-- `permission_demo.ml` - Attempts to trigger real permission requests
9797-- `discovery_demo.ml` - Shows permission discovery mode
9898-9999-## Important Notes
100100-101101-1. **Control Requests**: The permission callback is only invoked when Claude's CLI sends control requests. This typically happens when running with restricted permissions or specific security policies.
102102-103103-2. **Default Behavior**: In default mode, Claude may have broad permissions already, so callbacks might not be triggered for every tool use.
104104-105105-3. **Testing**: For testing permission callbacks, you can:
106106- - Use the simulated demo to test callback logic
107107- - Run Claude with restricted permissions via CLI flags
108108- - Use discovery mode to understand permission needs
109109-110110-4. **Security**: Permission callbacks are a security feature - always validate inputs and be cautious about what operations you allow.
+253
claudeio/TODO.md
···11+# TODO: Missing Features from Python SDK
22+33+## 1. Hook Support
44+55+### Overview
66+Hooks allow users to intercept and modify Claude's behavior at specific points during execution. The Python SDK supports several hook events that are not yet implemented in the OCaml library.
77+88+### Required Components
99+1010+#### Hook Events
1111+```ocaml
1212+type hook_event =
1313+ | Pre_tool_use (* Before a tool is invoked *)
1414+ | Post_tool_use (* After a tool completes *)
1515+ | User_prompt_submit (* When user submits a prompt *)
1616+ | Stop (* When stopping execution *)
1717+ | Subagent_stop (* When a subagent stops *)
1818+ | Pre_compact (* Before context compaction *)
1919+```
2020+2121+#### Hook Context
2222+```ocaml
2323+module Hook_context : sig
2424+ type t = {
2525+ signal : [ `Abort ] option; (* Future: abort signal support *)
2626+ }
2727+end
2828+```
2929+3030+#### Hook Output
3131+```ocaml
3232+module Hook_output : sig
3333+ type t = {
3434+ decision : [ `Block | `Continue ] option;
3535+ system_message : string option;
3636+ hook_specific_output : Ezjsonm.value option;
3737+ }
3838+end
3939+```
4040+4141+#### Hook Callback
4242+```ocaml
4343+type hook_callback =
4444+ input:Ezjsonm.value ->
4545+ tool_use_id:string option ->
4646+ context:Hook_context.t ->
4747+ Hook_output.t Eio.Promise.t
4848+```
4949+5050+#### Hook Matcher
5151+```ocaml
5252+module Hook_matcher : sig
5353+ type t = {
5454+ matcher : string option; (* e.g., "Bash" or "Write|MultiEdit|Edit" *)
5555+ hooks : hook_callback list;
5656+ }
5757+end
5858+```
5959+6060+### Implementation Plan
6161+6262+1. **Add hook types to a new `lib/hooks.mli` module**
6363+2. **Integrate hooks into `Options.t`**:
6464+ - Add `hooks : (hook_event * Hook_matcher.t list) list` field
6565+3. **Update `Client` module to handle hook callbacks**:
6666+ - Intercept tool use events
6767+ - Call registered hooks before/after operations
6868+ - Handle hook responses (block, modify, continue)
6969+4. **Update SDK control protocol** to support hook registration via `SDKControlInitializeRequest`
7070+7171+### Usage Example
7272+```ocaml
7373+let pre_tool_hook ~input ~tool_use_id:_ ~context:_ =
7474+ match Ezjsonm.find input ["name"] |> Ezjsonm.get_string with
7575+ | "Bash" ->
7676+ Eio.Promise.resolve Hook_output.{
7777+ decision = Some `Block;
7878+ system_message = Some "Bash commands blocked by hook";
7979+ hook_specific_output = None;
8080+ }
8181+ | _ ->
8282+ Eio.Promise.resolve Hook_output.{
8383+ decision = Some `Continue;
8484+ system_message = None;
8585+ hook_specific_output = None;
8686+ }
8787+8888+let options = Options.create
8989+ ~hooks:[
9090+ Pre_tool_use, [{
9191+ matcher = Some "Bash";
9292+ hooks = [pre_tool_hook]
9393+ }]
9494+ ]
9595+ ()
9696+```
9797+9898+## 2. MCP (Model Context Protocol) Server Support
9999+100100+### Overview
101101+MCP servers allow Claude to interact with external services and tools. The Python SDK supports multiple MCP server configurations.
102102+103103+### Required Components
104104+105105+#### MCP Server Types
106106+```ocaml
107107+module Mcp_server : sig
108108+ type stdio_config = {
109109+ command : string;
110110+ args : string list option;
111111+ env : (string * string) list option;
112112+ }
113113+114114+ type sse_config = {
115115+ url : string;
116116+ headers : (string * string) list option;
117117+ }
118118+119119+ type http_config = {
120120+ url : string;
121121+ headers : (string * string) list option;
122122+ }
123123+124124+ type sdk_config = {
125125+ name : string;
126126+ (* In OCaml, we'd need to define an MCP server interface *)
127127+ instance : mcp_server;
128128+ }
129129+130130+ and mcp_server = <
131131+ (* MCP server methods would go here *)
132132+ >
133133+134134+ type config =
135135+ | Stdio of stdio_config
136136+ | SSE of sse_config
137137+ | HTTP of http_config
138138+ | SDK of sdk_config
139139+end
140140+```
141141+142142+### Implementation Plan
143143+144144+1. **Create `lib/mcp.mli` module** with server configuration types
145145+2. **Add MCP support to `Options.t`**:
146146+ - Add `mcp_servers : (string * Mcp_server.config) list` field
147147+3. **Create MCP transport layer**:
148148+ - Stdio: Use Eio.Process for subprocess communication
149149+ - SSE: Use Cohttp and event stream parsing
150150+ - HTTP: Use Cohttp for REST API calls
151151+ - SDK: Direct OCaml object interface
152152+4. **Update SDK control protocol** to handle `SDKControlMcpMessageRequest`
153153+5. **Implement MCP message routing** in Client module
154154+155155+### Usage Example
156156+```ocaml
157157+let stdio_server = Mcp_server.Stdio {
158158+ command = "calculator-server";
159159+ args = Some ["--mode", "advanced"];
160160+ env = None;
161161+}
162162+163163+let http_server = Mcp_server.HTTP {
164164+ url = "https://api.example.com/mcp";
165165+ headers = Some [("Authorization", "Bearer token")];
166166+}
167167+168168+let options = Options.create
169169+ ~mcp_servers:[
170170+ "calculator", stdio_server;
171171+ "api", http_server;
172172+ ]
173173+ ()
174174+```
175175+176176+### MCP Message Flow
177177+178178+1. Claude requests tool use from MCP server
179179+2. Client sends `mcp_message` control request
180180+3. SDK routes message to appropriate MCP server
181181+4. MCP server responds with result
182182+5. Client forwards result back to Claude
183183+184184+## 3. Integration with SDK Control Protocol
185185+186186+Both hooks and MCP will require updates to the SDK control protocol:
187187+188188+### Control Request Types
189189+```ocaml
190190+module Sdk_control : sig
191191+ type interrupt_request = {
192192+ subtype : [`Interrupt];
193193+ }
194194+195195+ type permission_request = {
196196+ subtype : [`Can_use_tool];
197197+ tool_name : string;
198198+ input : Ezjsonm.value;
199199+ permission_suggestions : Permissions.Update.t list option;
200200+ blocked_path : string option;
201201+ }
202202+203203+ type initialize_request = {
204204+ subtype : [`Initialize];
205205+ hooks : (hook_event * Ezjsonm.value) list option;
206206+ }
207207+208208+ type set_permission_mode_request = {
209209+ subtype : [`Set_permission_mode];
210210+ mode : Permissions.Mode.t;
211211+ }
212212+213213+ type hook_callback_request = {
214214+ subtype : [`Hook_callback];
215215+ callback_id : string;
216216+ input : Ezjsonm.value;
217217+ tool_use_id : string option;
218218+ }
219219+220220+ type mcp_message_request = {
221221+ subtype : [`Mcp_message];
222222+ server_name : string;
223223+ message : Ezjsonm.value;
224224+ }
225225+226226+ type request =
227227+ | Interrupt of interrupt_request
228228+ | Permission of permission_request
229229+ | Initialize of initialize_request
230230+ | Set_permission_mode of set_permission_mode_request
231231+ | Hook_callback of hook_callback_request
232232+ | Mcp_message of mcp_message_request
233233+end
234234+```
235235+236236+## Implementation Priority
237237+238238+1. **Phase 1**: Implement typed SDK control protocol (prerequisite for both)
239239+2. **Phase 2**: Implement hook support (simpler, self-contained)
240240+3. **Phase 3**: Implement MCP server support (requires external dependencies)
241241+242242+## Testing Strategy
243243+244244+### Hooks
245245+- Unit tests for hook registration and matching
246246+- Integration tests with mock tool invocations
247247+- Test hook blocking, modification, and pass-through scenarios
248248+249249+### MCP
250250+- Unit tests for configuration parsing
251251+- Mock MCP server for integration testing
252252+- Test different transport types (stdio, HTTP, SSE)
253253+- Test message routing and error handling
+112
claudeio/test/TEST.md
···11+# Claude Library Architecture Summary
22+33+This document summarizes the architecture of the OCaml Eio Claude library located in `../lib`.
44+55+## Overview
66+77+The Claude library is a high-quality OCaml Eio wrapper around the Claude Code CLI that provides structured JSON streaming communication with Claude. It follows a clean layered architecture with strong typing and comprehensive error handling.
88+99+## Core Architecture
1010+1111+The library is organized into several focused modules that work together to provide a complete Claude integration:
1212+1313+### 1. Transport Layer (`Transport`)
1414+- **Purpose**: Low-level CLI process management and communication
1515+- **Key Functions**:
1616+ - Spawns and manages the `claude` CLI process using Eio's process manager
1717+ - Handles bidirectional JSON streaming via stdin/stdout
1818+ - Provides `send`/`receive_line` primitives with proper resource cleanup
1919+- **Integration**: Forms the foundation for all Claude communication
2020+2121+### 2. Message Protocol Layer
2222+2323+#### Content Blocks (`Content_block`)
2424+- **Purpose**: Defines the building blocks of Claude messages
2525+- **Types**: Text, Tool_use, Tool_result, Thinking blocks
2626+- **Key Features**: Each block type has specialized accessors and JSON serialization
2727+- **Integration**: Used by messages to represent diverse content types
2828+2929+#### Messages (`Message`)
3030+- **Purpose**: Structured message types for Claude communication
3131+- **Types**: User, Assistant, System, Result messages
3232+- **Key Features**:
3333+ - User messages support both simple strings and complex content blocks
3434+ - Assistant messages include model info and mixed content
3535+ - System messages handle session control
3636+ - Result messages provide conversation metadata and usage stats
3737+- **Integration**: Primary data structures exchanged between client and Claude
3838+3939+#### Control Messages (`Control`)
4040+- **Purpose**: Session management and control flow
4141+- **Key Features**: Request IDs, subtypes, and arbitrary JSON data payload
4242+- **Integration**: Used for session initialization, cancellation, and other operational commands
4343+4444+### 3. Permission System (`Permissions`)
4545+- **Purpose**: Fine-grained control over Claude's tool usage
4646+- **Components**:
4747+ - **Modes**: Default, Accept_edits, Plan, Bypass_permissions
4848+ - **Rules**: Tool-specific permission specifications
4949+ - **Callbacks**: Custom permission logic with context and suggestions
5050+ - **Results**: Allow/Deny decisions with optional modifications
5151+- **Integration**: Consulted by client before allowing tool invocations
5252+5353+### 4. Configuration (`Options`)
5454+- **Purpose**: Session configuration and behavior control
5555+- **Features**:
5656+ - Tool allow/disallow lists
5757+ - System prompt customization (replace or append)
5858+ - Model selection and thinking token limits
5959+ - Working directory and environment variables
6060+- **Integration**: Passed to transport layer and used throughout the session
6161+- **Pattern**: Builder pattern with `with_*` functions for immutable updates
6262+6363+### 5. Client Interface (`Client`)
6464+- **Purpose**: High-level API for Claude interactions
6565+- **Key Functions**:
6666+ - Session creation and management
6767+ - Message sending (`query`, `send_message`, `send_user_message`)
6868+ - Response streaming (`receive`, `receive_all`)
6969+ - Permission discovery and callback management
7070+- **Integration**: Orchestrates all other modules to provide the main user API
7171+7272+### 6. Main Module (`Claude`)
7373+- **Purpose**: Public API facade with comprehensive documentation
7474+- **Features**:
7575+ - Re-exports all sub-modules
7676+ - Extensive usage examples and architectural documentation
7777+ - Logging configuration guidance
7878+- **Integration**: Single entry point for library users
7979+8080+## Data Flow
8181+8282+1. **Configuration**: Options are created with desired settings
8383+2. **Transport**: Client creates transport layer with CLI process
8484+3. **Message Exchange**:
8585+ - User messages are sent via JSON streaming
8686+ - Claude responses are received as streaming JSON
8787+ - Messages are parsed into strongly-typed structures
8888+4. **Permission Checking**: Tool usage is filtered through permission system
8989+5. **Content Processing**: Response content blocks are extracted and processed
9090+6. **Session Management**: Control messages handle session lifecycle
9191+9292+## Key Design Principles
9393+9494+- **Eio Integration**: Native use of Eio's concurrency primitives (Switch, Process.mgr)
9595+- **Type Safety**: Comprehensive typing with specific error exceptions
9696+- **Streaming**: Efficient processing via `Message.t Seq.t` sequences
9797+- **Modularity**: Clear separation of concerns with minimal inter-dependencies
9898+- **Documentation**: Extensive interface documentation with usage examples
9999+- **Error Handling**: Specific exception types for different failure modes
100100+- **Logging**: Structured logging with per-module sources using the Logs library
101101+102102+## Usage Patterns
103103+104104+The library supports both simple text queries and complex multi-turn conversations:
105105+106106+- **Simple Queries**: `Client.query` with text input
107107+- **Tool Control**: Permission callbacks and allow/disallow lists
108108+- **Streaming**: Process responses as they arrive via sequences
109109+- **Session Management**: Full control over Claude's execution environment
110110+- **Custom Prompts**: System prompt replacement and augmentation
111111+112112+The architecture enables fine-grained control over Claude's capabilities while maintaining ease of use for common scenarios.
+185
claudeio/test/permission_demo.py
···11+#!/usr/bin/env python3
22+# /// script
33+# requires-python = ">=3.9"
44+# dependencies = [
55+# "claude-code-sdk",
66+# ]
77+# ///
88+"""
99+Permission demo for Claude Code SDK Python.
1010+Demonstrates how the permission callback system works.
1111+"""
1212+1313+import asyncio
1414+import sys
1515+import logging
1616+from typing import Any, Dict
1717+1818+from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions
1919+from claude_code_sdk.types import (
2020+ PermissionResultAllow,
2121+ PermissionResultDeny,
2222+ ToolPermissionContext,
2323+)
2424+2525+# Set up logging
2626+logging.basicConfig(
2727+ level=logging.INFO,
2828+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
2929+)
3030+logger = logging.getLogger(__name__)
3131+3232+# Track granted permissions
3333+granted_permissions = set()
3434+3535+3636+async def interactive_permission_callback(
3737+ tool_name: str,
3838+ tool_input: Dict[str, Any],
3939+ context: ToolPermissionContext
4040+) -> PermissionResultAllow | PermissionResultDeny:
4141+ """Interactive permission callback that asks user for permission."""
4242+4343+ logger.info(f"🔔 Permission callback invoked for tool: {tool_name}")
4444+ print(f"\n🔐 PERMISSION REQUEST 🔐")
4545+ print(f"Tool: {tool_name}")
4646+4747+ # Log the full input for debugging
4848+ logger.info(f"Full input: {tool_input}")
4949+5050+ # Show input details
5151+ try:
5252+ if tool_name == "Read":
5353+ file_path = tool_input.get("file_path", "")
5454+ print(f"File: {file_path}")
5555+ elif tool_name == "Bash":
5656+ command = tool_input.get("command", "")
5757+ print(f"Command: {command}")
5858+ elif tool_name in ["Write", "Edit"]:
5959+ file_path = tool_input.get("file_path", "")
6060+ print(f"File: {file_path}")
6161+ elif tool_name == "Glob":
6262+ pattern = tool_input.get("pattern", "")
6363+ path = tool_input.get("path", "(current directory)")
6464+ print(f"Pattern: {pattern}")
6565+ print(f"Path: {path}")
6666+ elif tool_name == "Grep":
6767+ pattern = tool_input.get("pattern", "")
6868+ path = tool_input.get("path", "(current directory)")
6969+ print(f"Pattern: {pattern}")
7070+ print(f"Path: {path}")
7171+ else:
7272+ print(f"Input: {tool_input}")
7373+ except Exception as e:
7474+ logger.info(f"Failed to parse input details: {e}")
7575+7676+ # Check if already granted
7777+ if tool_name in granted_permissions:
7878+ print("→ Auto-approved (previously granted)")
7979+ logger.info(f"Returning allow result for {tool_name}")
8080+ return PermissionResultAllow()
8181+8282+ # Ask user
8383+ response = input("Allow? [y/N/always]: ").lower().strip()
8484+8585+ if response in ["y", "yes"]:
8686+ print("→ Allowed (this time only)")
8787+ logger.info(f"User approved {tool_name} for this request only")
8888+ return PermissionResultAllow()
8989+ elif response in ["a", "always"]:
9090+ granted_permissions.add(tool_name)
9191+ print(f"✅ Permission granted for: {tool_name}")
9292+ logger.info(f"User granted permanent permission for {tool_name}")
9393+ return PermissionResultAllow()
9494+ else:
9595+ print(f"❌ Permission denied for: {tool_name}")
9696+ logger.info(f"User denied permission for {tool_name}")
9797+ return PermissionResultDeny(
9898+ message=f"User denied access to {tool_name}",
9999+ interrupt=False
100100+ )
101101+102102+103103+async def run_demo():
104104+ """Run the permission demo."""
105105+ print("🚀 Starting Permission Demo")
106106+ print("==================================")
107107+ print("This demo starts with NO permissions.")
108108+ print("Claude will request permissions as needed.\n")
109109+110110+ # Create options with custom permission callback
111111+ # Test WITHOUT allowed_tools to see if permission requests come through
112112+ options = ClaudeCodeOptions(
113113+ model="sonnet",
114114+ # allowed_tools=["Read", "Write", "Bash", "Edit", "Glob", "Grep"],
115115+ can_use_tool=interactive_permission_callback,
116116+ )
117117+118118+ async with ClaudeSDKClient(options=options) as client:
119119+ # First prompt - Claude will need to request Read permission
120120+ print("\n📤 Sending first prompt (reading from ../lib)...")
121121+ messages = []
122122+ await client.query(
123123+ "Please read and analyze the source files in the ../lib directory. "
124124+ "Focus on the main OCaml modules and their purpose. "
125125+ "What is the overall architecture of this Claude library?"
126126+ )
127127+128128+ async for msg in client.receive_response():
129129+ messages.append(msg)
130130+ if hasattr(msg, 'content'):
131131+ if isinstance(msg.content, str):
132132+ print(f"\n📝 Claude says:\n{msg.content}")
133133+ elif isinstance(msg.content, list):
134134+ for block in msg.content:
135135+ if hasattr(block, 'text'):
136136+ print(f"\n📝 Claude says:\n{block.text}")
137137+138138+ # Show current permissions
139139+ print("\n📋 Current permission status:")
140140+ if granted_permissions:
141141+ print(f"Currently granted permissions: {', '.join(granted_permissions)}")
142142+ else:
143143+ print("No permissions granted yet")
144144+145145+ # Second prompt - will need Write permission
146146+ print("\n📤 Sending second prompt (writing TEST.md)...")
147147+ await client.query(
148148+ "Now write a summary of what you learned about the Claude library "
149149+ "architecture to a file called TEST.md in the current directory. "
150150+ "Include the main modules, their purposes, and how they work together."
151151+ )
152152+153153+ async for msg in client.receive_response():
154154+ if hasattr(msg, 'content'):
155155+ if isinstance(msg.content, str):
156156+ print(f"\n📝 Claude says:\n{msg.content}")
157157+ elif isinstance(msg.content, list):
158158+ for block in msg.content:
159159+ if hasattr(block, 'text'):
160160+ print(f"\n📝 Claude says:\n{block.text}")
161161+162162+ # Show final permissions
163163+ print("\n📋 Final permission status:")
164164+ if granted_permissions:
165165+ print(f"Currently granted permissions: {', '.join(granted_permissions)}")
166166+ else:
167167+ print("No permissions granted yet")
168168+169169+ print("\n==================================")
170170+ print("✨ Demo complete!")
171171+172172+173173+async def main():
174174+ """Main entry point."""
175175+ try:
176176+ await run_demo()
177177+ except KeyboardInterrupt:
178178+ print("\n\nDemo interrupted by user.")
179179+ except Exception as e:
180180+ logger.error(f"Error in demo: {e}", exc_info=True)
181181+ sys.exit(1)
182182+183183+184184+if __name__ == "__main__":
185185+ asyncio.run(main())