My agentic slop goes here. Not intended for anyone else!

sync

Changed files
+550 -110
claudeio
-110
claudeio/PERMISSIONS.md
···
-
# Claude OCaml SDK Permission System
-
-
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.
-
-
## Overview
-
-
The permission system allows you to:
-
- Control which tools Claude can use
-
- Implement custom permission callbacks
-
- Discover what permissions an operation needs
-
- Grant or deny permissions dynamically
-
-
## Permission Modes
-
-
Claude supports several permission modes:
-
- `Default` - Standard permission checks
-
- `Accept_edits` - Automatically accept file edits
-
- `Plan` - Planning mode with restricted execution
-
- `Bypass_permissions` - Skip all permission checks
-
-
## Using Permission Callbacks
-
-
### Basic Callback
-
-
```ocaml
-
let my_permission_callback ~tool_name ~input ~context =
-
match tool_name with
-
| "Read" ->
-
(* Always allow reading *)
-
Claude.Permissions.Result.allow ()
-
| "Write" | "Edit" ->
-
(* Deny write operations *)
-
Claude.Permissions.Result.deny
-
~message:"Write operations are not allowed"
-
~interrupt:false
-
| _ ->
-
(* Allow everything else *)
-
Claude.Permissions.Result.allow ()
-
-
let options = Claude.Options.create
-
~permission_callback:my_permission_callback
-
() in
-
```
-
-
### Interactive Callback
-
-
See `test/simulated_permissions.ml` for an example that prompts the user interactively.
-
-
### Discovery Mode
-
-
The discovery callback helps you understand what permissions an operation needs:
-
-
```ocaml
-
let client = Claude.Client.discover_permissions client in
-
(* Run operations *)
-
let discovered = Claude.Client.get_discovered_permissions client in
-
List.iter (fun rule ->
-
Printf.printf "Tool needed: %s\n"
-
(Claude.Permissions.Rule.tool_name rule)
-
) discovered
-
```
-
-
## Permission Results
-
-
Callbacks return permission results that can:
-
- Allow the operation (optionally with modified input)
-
- Deny the operation (with a message and optional interrupt)
-
-
```ocaml
-
(* Allow with modified input *)
-
Claude.Permissions.Result.allow
-
~updated_input:(Ezjsonm.dict ["sanitized", Ezjsonm.string "data"])
-
()
-
-
(* Deny and stop execution *)
-
Claude.Permissions.Result.deny
-
~message:"Security policy violation"
-
~interrupt:true
-
```
-
-
## Tool Restrictions
-
-
You can also restrict tools at the options level:
-
-
```ocaml
-
let options = Claude.Options.create
-
~allowed_tools:["Read"; "Grep"] (* Only these tools *)
-
~disallowed_tools:["Bash"; "Write"] (* Block these *)
-
()
-
```
-
-
## Examples
-
-
The `test/` directory contains several examples:
-
- `simulated_permissions.ml` - Interactive permission demo
-
- `permission_demo.ml` - Attempts to trigger real permission requests
-
- `discovery_demo.ml` - Shows permission discovery mode
-
-
## Important Notes
-
-
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.
-
-
2. **Default Behavior**: In default mode, Claude may have broad permissions already, so callbacks might not be triggered for every tool use.
-
-
3. **Testing**: For testing permission callbacks, you can:
-
- Use the simulated demo to test callback logic
-
- Run Claude with restricted permissions via CLI flags
-
- Use discovery mode to understand permission needs
-
-
4. **Security**: Permission callbacks are a security feature - always validate inputs and be cautious about what operations you allow.
+253
claudeio/TODO.md
···
+
# TODO: Missing Features from Python SDK
+
+
## 1. Hook Support
+
+
### Overview
+
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.
+
+
### Required Components
+
+
#### Hook Events
+
```ocaml
+
type hook_event =
+
| Pre_tool_use (* Before a tool is invoked *)
+
| Post_tool_use (* After a tool completes *)
+
| User_prompt_submit (* When user submits a prompt *)
+
| Stop (* When stopping execution *)
+
| Subagent_stop (* When a subagent stops *)
+
| Pre_compact (* Before context compaction *)
+
```
+
+
#### Hook Context
+
```ocaml
+
module Hook_context : sig
+
type t = {
+
signal : [ `Abort ] option; (* Future: abort signal support *)
+
}
+
end
+
```
+
+
#### Hook Output
+
```ocaml
+
module Hook_output : sig
+
type t = {
+
decision : [ `Block | `Continue ] option;
+
system_message : string option;
+
hook_specific_output : Ezjsonm.value option;
+
}
+
end
+
```
+
+
#### Hook Callback
+
```ocaml
+
type hook_callback =
+
input:Ezjsonm.value ->
+
tool_use_id:string option ->
+
context:Hook_context.t ->
+
Hook_output.t Eio.Promise.t
+
```
+
+
#### Hook Matcher
+
```ocaml
+
module Hook_matcher : sig
+
type t = {
+
matcher : string option; (* e.g., "Bash" or "Write|MultiEdit|Edit" *)
+
hooks : hook_callback list;
+
}
+
end
+
```
+
+
### Implementation Plan
+
+
1. **Add hook types to a new `lib/hooks.mli` module**
+
2. **Integrate hooks into `Options.t`**:
+
- Add `hooks : (hook_event * Hook_matcher.t list) list` field
+
3. **Update `Client` module to handle hook callbacks**:
+
- Intercept tool use events
+
- Call registered hooks before/after operations
+
- Handle hook responses (block, modify, continue)
+
4. **Update SDK control protocol** to support hook registration via `SDKControlInitializeRequest`
+
+
### Usage Example
+
```ocaml
+
let pre_tool_hook ~input ~tool_use_id:_ ~context:_ =
+
match Ezjsonm.find input ["name"] |> Ezjsonm.get_string with
+
| "Bash" ->
+
Eio.Promise.resolve Hook_output.{
+
decision = Some `Block;
+
system_message = Some "Bash commands blocked by hook";
+
hook_specific_output = None;
+
}
+
| _ ->
+
Eio.Promise.resolve Hook_output.{
+
decision = Some `Continue;
+
system_message = None;
+
hook_specific_output = None;
+
}
+
+
let options = Options.create
+
~hooks:[
+
Pre_tool_use, [{
+
matcher = Some "Bash";
+
hooks = [pre_tool_hook]
+
}]
+
]
+
()
+
```
+
+
## 2. MCP (Model Context Protocol) Server Support
+
+
### Overview
+
MCP servers allow Claude to interact with external services and tools. The Python SDK supports multiple MCP server configurations.
+
+
### Required Components
+
+
#### MCP Server Types
+
```ocaml
+
module Mcp_server : sig
+
type stdio_config = {
+
command : string;
+
args : string list option;
+
env : (string * string) list option;
+
}
+
+
type sse_config = {
+
url : string;
+
headers : (string * string) list option;
+
}
+
+
type http_config = {
+
url : string;
+
headers : (string * string) list option;
+
}
+
+
type sdk_config = {
+
name : string;
+
(* In OCaml, we'd need to define an MCP server interface *)
+
instance : mcp_server;
+
}
+
+
and mcp_server = <
+
(* MCP server methods would go here *)
+
>
+
+
type config =
+
| Stdio of stdio_config
+
| SSE of sse_config
+
| HTTP of http_config
+
| SDK of sdk_config
+
end
+
```
+
+
### Implementation Plan
+
+
1. **Create `lib/mcp.mli` module** with server configuration types
+
2. **Add MCP support to `Options.t`**:
+
- Add `mcp_servers : (string * Mcp_server.config) list` field
+
3. **Create MCP transport layer**:
+
- Stdio: Use Eio.Process for subprocess communication
+
- SSE: Use Cohttp and event stream parsing
+
- HTTP: Use Cohttp for REST API calls
+
- SDK: Direct OCaml object interface
+
4. **Update SDK control protocol** to handle `SDKControlMcpMessageRequest`
+
5. **Implement MCP message routing** in Client module
+
+
### Usage Example
+
```ocaml
+
let stdio_server = Mcp_server.Stdio {
+
command = "calculator-server";
+
args = Some ["--mode", "advanced"];
+
env = None;
+
}
+
+
let http_server = Mcp_server.HTTP {
+
url = "https://api.example.com/mcp";
+
headers = Some [("Authorization", "Bearer token")];
+
}
+
+
let options = Options.create
+
~mcp_servers:[
+
"calculator", stdio_server;
+
"api", http_server;
+
]
+
()
+
```
+
+
### MCP Message Flow
+
+
1. Claude requests tool use from MCP server
+
2. Client sends `mcp_message` control request
+
3. SDK routes message to appropriate MCP server
+
4. MCP server responds with result
+
5. Client forwards result back to Claude
+
+
## 3. Integration with SDK Control Protocol
+
+
Both hooks and MCP will require updates to the SDK control protocol:
+
+
### Control Request Types
+
```ocaml
+
module Sdk_control : sig
+
type interrupt_request = {
+
subtype : [`Interrupt];
+
}
+
+
type permission_request = {
+
subtype : [`Can_use_tool];
+
tool_name : string;
+
input : Ezjsonm.value;
+
permission_suggestions : Permissions.Update.t list option;
+
blocked_path : string option;
+
}
+
+
type initialize_request = {
+
subtype : [`Initialize];
+
hooks : (hook_event * Ezjsonm.value) list option;
+
}
+
+
type set_permission_mode_request = {
+
subtype : [`Set_permission_mode];
+
mode : Permissions.Mode.t;
+
}
+
+
type hook_callback_request = {
+
subtype : [`Hook_callback];
+
callback_id : string;
+
input : Ezjsonm.value;
+
tool_use_id : string option;
+
}
+
+
type mcp_message_request = {
+
subtype : [`Mcp_message];
+
server_name : string;
+
message : Ezjsonm.value;
+
}
+
+
type request =
+
| Interrupt of interrupt_request
+
| Permission of permission_request
+
| Initialize of initialize_request
+
| Set_permission_mode of set_permission_mode_request
+
| Hook_callback of hook_callback_request
+
| Mcp_message of mcp_message_request
+
end
+
```
+
+
## Implementation Priority
+
+
1. **Phase 1**: Implement typed SDK control protocol (prerequisite for both)
+
2. **Phase 2**: Implement hook support (simpler, self-contained)
+
3. **Phase 3**: Implement MCP server support (requires external dependencies)
+
+
## Testing Strategy
+
+
### Hooks
+
- Unit tests for hook registration and matching
+
- Integration tests with mock tool invocations
+
- Test hook blocking, modification, and pass-through scenarios
+
+
### MCP
+
- Unit tests for configuration parsing
+
- Mock MCP server for integration testing
+
- Test different transport types (stdio, HTTP, SSE)
+
- Test message routing and error handling
+112
claudeio/test/TEST.md
···
+
# Claude Library Architecture Summary
+
+
This document summarizes the architecture of the OCaml Eio Claude library located in `../lib`.
+
+
## Overview
+
+
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.
+
+
## Core Architecture
+
+
The library is organized into several focused modules that work together to provide a complete Claude integration:
+
+
### 1. Transport Layer (`Transport`)
+
- **Purpose**: Low-level CLI process management and communication
+
- **Key Functions**:
+
- Spawns and manages the `claude` CLI process using Eio's process manager
+
- Handles bidirectional JSON streaming via stdin/stdout
+
- Provides `send`/`receive_line` primitives with proper resource cleanup
+
- **Integration**: Forms the foundation for all Claude communication
+
+
### 2. Message Protocol Layer
+
+
#### Content Blocks (`Content_block`)
+
- **Purpose**: Defines the building blocks of Claude messages
+
- **Types**: Text, Tool_use, Tool_result, Thinking blocks
+
- **Key Features**: Each block type has specialized accessors and JSON serialization
+
- **Integration**: Used by messages to represent diverse content types
+
+
#### Messages (`Message`)
+
- **Purpose**: Structured message types for Claude communication
+
- **Types**: User, Assistant, System, Result messages
+
- **Key Features**:
+
- User messages support both simple strings and complex content blocks
+
- Assistant messages include model info and mixed content
+
- System messages handle session control
+
- Result messages provide conversation metadata and usage stats
+
- **Integration**: Primary data structures exchanged between client and Claude
+
+
#### Control Messages (`Control`)
+
- **Purpose**: Session management and control flow
+
- **Key Features**: Request IDs, subtypes, and arbitrary JSON data payload
+
- **Integration**: Used for session initialization, cancellation, and other operational commands
+
+
### 3. Permission System (`Permissions`)
+
- **Purpose**: Fine-grained control over Claude's tool usage
+
- **Components**:
+
- **Modes**: Default, Accept_edits, Plan, Bypass_permissions
+
- **Rules**: Tool-specific permission specifications
+
- **Callbacks**: Custom permission logic with context and suggestions
+
- **Results**: Allow/Deny decisions with optional modifications
+
- **Integration**: Consulted by client before allowing tool invocations
+
+
### 4. Configuration (`Options`)
+
- **Purpose**: Session configuration and behavior control
+
- **Features**:
+
- Tool allow/disallow lists
+
- System prompt customization (replace or append)
+
- Model selection and thinking token limits
+
- Working directory and environment variables
+
- **Integration**: Passed to transport layer and used throughout the session
+
- **Pattern**: Builder pattern with `with_*` functions for immutable updates
+
+
### 5. Client Interface (`Client`)
+
- **Purpose**: High-level API for Claude interactions
+
- **Key Functions**:
+
- Session creation and management
+
- Message sending (`query`, `send_message`, `send_user_message`)
+
- Response streaming (`receive`, `receive_all`)
+
- Permission discovery and callback management
+
- **Integration**: Orchestrates all other modules to provide the main user API
+
+
### 6. Main Module (`Claude`)
+
- **Purpose**: Public API facade with comprehensive documentation
+
- **Features**:
+
- Re-exports all sub-modules
+
- Extensive usage examples and architectural documentation
+
- Logging configuration guidance
+
- **Integration**: Single entry point for library users
+
+
## Data Flow
+
+
1. **Configuration**: Options are created with desired settings
+
2. **Transport**: Client creates transport layer with CLI process
+
3. **Message Exchange**:
+
- User messages are sent via JSON streaming
+
- Claude responses are received as streaming JSON
+
- Messages are parsed into strongly-typed structures
+
4. **Permission Checking**: Tool usage is filtered through permission system
+
5. **Content Processing**: Response content blocks are extracted and processed
+
6. **Session Management**: Control messages handle session lifecycle
+
+
## Key Design Principles
+
+
- **Eio Integration**: Native use of Eio's concurrency primitives (Switch, Process.mgr)
+
- **Type Safety**: Comprehensive typing with specific error exceptions
+
- **Streaming**: Efficient processing via `Message.t Seq.t` sequences
+
- **Modularity**: Clear separation of concerns with minimal inter-dependencies
+
- **Documentation**: Extensive interface documentation with usage examples
+
- **Error Handling**: Specific exception types for different failure modes
+
- **Logging**: Structured logging with per-module sources using the Logs library
+
+
## Usage Patterns
+
+
The library supports both simple text queries and complex multi-turn conversations:
+
+
- **Simple Queries**: `Client.query` with text input
+
- **Tool Control**: Permission callbacks and allow/disallow lists
+
- **Streaming**: Process responses as they arrive via sequences
+
- **Session Management**: Full control over Claude's execution environment
+
- **Custom Prompts**: System prompt replacement and augmentation
+
+
The architecture enables fine-grained control over Claude's capabilities while maintaining ease of use for common scenarios.
+185
claudeio/test/permission_demo.py
···
+
#!/usr/bin/env python3
+
# /// script
+
# requires-python = ">=3.9"
+
# dependencies = [
+
# "claude-code-sdk",
+
# ]
+
# ///
+
"""
+
Permission demo for Claude Code SDK Python.
+
Demonstrates how the permission callback system works.
+
"""
+
+
import asyncio
+
import sys
+
import logging
+
from typing import Any, Dict
+
+
from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions
+
from claude_code_sdk.types import (
+
PermissionResultAllow,
+
PermissionResultDeny,
+
ToolPermissionContext,
+
)
+
+
# Set up logging
+
logging.basicConfig(
+
level=logging.INFO,
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+
)
+
logger = logging.getLogger(__name__)
+
+
# Track granted permissions
+
granted_permissions = set()
+
+
+
async def interactive_permission_callback(
+
tool_name: str,
+
tool_input: Dict[str, Any],
+
context: ToolPermissionContext
+
) -> PermissionResultAllow | PermissionResultDeny:
+
"""Interactive permission callback that asks user for permission."""
+
+
logger.info(f"🔔 Permission callback invoked for tool: {tool_name}")
+
print(f"\n🔐 PERMISSION REQUEST 🔐")
+
print(f"Tool: {tool_name}")
+
+
# Log the full input for debugging
+
logger.info(f"Full input: {tool_input}")
+
+
# Show input details
+
try:
+
if tool_name == "Read":
+
file_path = tool_input.get("file_path", "")
+
print(f"File: {file_path}")
+
elif tool_name == "Bash":
+
command = tool_input.get("command", "")
+
print(f"Command: {command}")
+
elif tool_name in ["Write", "Edit"]:
+
file_path = tool_input.get("file_path", "")
+
print(f"File: {file_path}")
+
elif tool_name == "Glob":
+
pattern = tool_input.get("pattern", "")
+
path = tool_input.get("path", "(current directory)")
+
print(f"Pattern: {pattern}")
+
print(f"Path: {path}")
+
elif tool_name == "Grep":
+
pattern = tool_input.get("pattern", "")
+
path = tool_input.get("path", "(current directory)")
+
print(f"Pattern: {pattern}")
+
print(f"Path: {path}")
+
else:
+
print(f"Input: {tool_input}")
+
except Exception as e:
+
logger.info(f"Failed to parse input details: {e}")
+
+
# Check if already granted
+
if tool_name in granted_permissions:
+
print("→ Auto-approved (previously granted)")
+
logger.info(f"Returning allow result for {tool_name}")
+
return PermissionResultAllow()
+
+
# Ask user
+
response = input("Allow? [y/N/always]: ").lower().strip()
+
+
if response in ["y", "yes"]:
+
print("→ Allowed (this time only)")
+
logger.info(f"User approved {tool_name} for this request only")
+
return PermissionResultAllow()
+
elif response in ["a", "always"]:
+
granted_permissions.add(tool_name)
+
print(f"✅ Permission granted for: {tool_name}")
+
logger.info(f"User granted permanent permission for {tool_name}")
+
return PermissionResultAllow()
+
else:
+
print(f"❌ Permission denied for: {tool_name}")
+
logger.info(f"User denied permission for {tool_name}")
+
return PermissionResultDeny(
+
message=f"User denied access to {tool_name}",
+
interrupt=False
+
)
+
+
+
async def run_demo():
+
"""Run the permission demo."""
+
print("🚀 Starting Permission Demo")
+
print("==================================")
+
print("This demo starts with NO permissions.")
+
print("Claude will request permissions as needed.\n")
+
+
# Create options with custom permission callback
+
# Test WITHOUT allowed_tools to see if permission requests come through
+
options = ClaudeCodeOptions(
+
model="sonnet",
+
# allowed_tools=["Read", "Write", "Bash", "Edit", "Glob", "Grep"],
+
can_use_tool=interactive_permission_callback,
+
)
+
+
async with ClaudeSDKClient(options=options) as client:
+
# First prompt - Claude will need to request Read permission
+
print("\n📤 Sending first prompt (reading from ../lib)...")
+
messages = []
+
await client.query(
+
"Please read and analyze the source files in the ../lib directory. "
+
"Focus on the main OCaml modules and their purpose. "
+
"What is the overall architecture of this Claude library?"
+
)
+
+
async for msg in client.receive_response():
+
messages.append(msg)
+
if hasattr(msg, 'content'):
+
if isinstance(msg.content, str):
+
print(f"\n📝 Claude says:\n{msg.content}")
+
elif isinstance(msg.content, list):
+
for block in msg.content:
+
if hasattr(block, 'text'):
+
print(f"\n📝 Claude says:\n{block.text}")
+
+
# Show current permissions
+
print("\n📋 Current permission status:")
+
if granted_permissions:
+
print(f"Currently granted permissions: {', '.join(granted_permissions)}")
+
else:
+
print("No permissions granted yet")
+
+
# Second prompt - will need Write permission
+
print("\n📤 Sending second prompt (writing TEST.md)...")
+
await client.query(
+
"Now write a summary of what you learned about the Claude library "
+
"architecture to a file called TEST.md in the current directory. "
+
"Include the main modules, their purposes, and how they work together."
+
)
+
+
async for msg in client.receive_response():
+
if hasattr(msg, 'content'):
+
if isinstance(msg.content, str):
+
print(f"\n📝 Claude says:\n{msg.content}")
+
elif isinstance(msg.content, list):
+
for block in msg.content:
+
if hasattr(block, 'text'):
+
print(f"\n📝 Claude says:\n{block.text}")
+
+
# Show final permissions
+
print("\n📋 Final permission status:")
+
if granted_permissions:
+
print(f"Currently granted permissions: {', '.join(granted_permissions)}")
+
else:
+
print("No permissions granted yet")
+
+
print("\n==================================")
+
print("✨ Demo complete!")
+
+
+
async def main():
+
"""Main entry point."""
+
try:
+
await run_demo()
+
except KeyboardInterrupt:
+
print("\n\nDemo interrupted by user.")
+
except Exception as e:
+
logger.error(f"Error in demo: {e}", exc_info=True)
+
sys.exit(1)
+
+
+
if __name__ == "__main__":
+
asyncio.run(main())