ClaudeOCaml Eio library for Claude Code CLI.
This library provides an interface to the Claude Code command-line interface using OCaml's Eio concurrency library. It wraps Claude CLI invocations with JSON streaming for asynchronous communication.
The Claude library enables you to:
The library is structured into two layers:
Client: High-level client interface for interacting with ClaudeResponse: High-level response events from ClaudeHandler: Object-oriented response handler with sensible defaultsOptions: Configuration options for Claude sessionsPermissions: Fine-grained permission system for tool usageHooks: Fully typed hook callbacks for event interceptionContent_block: Content blocks (text, tool use, tool results, thinking)Message: Messages exchanged with Claude (user, assistant, system, result)Tool_input: Opaque tool input with typed accessorsServer_info: Server capabilities and metadataProto: Direct access to wire-format types and JSON codecs open Eio.Std
let () =
Eio_main.run @@ fun env ->
Switch.run @@ fun sw ->
let client =
Claude.Client.create ~sw ~process_mgr:(Eio.Stdenv.process_mgr env) ()
in
Claude.Client.query client "What is 2+2?";
let handler =
object
inherit Claude.Handler.default
method! on_text t = print_endline (Claude.Response.Text.content t)
end
in
Claude.Client.run client ~handlerThe library provides two ways to handle responses:
Subclass Handler.default and override only the methods you need:
let my_handler =
object
inherit Claude.Handler.default
method! on_text t = print_endline (Claude.Response.Text.content t)
method! on_tool_use t =
Printf.printf "Tool: %s\n" (Claude.Response.Tool_use.name t)
method! on_complete c =
Printf.printf "Done! Cost: $%.4f\n"
(Option.value ~default:0.0
(Claude.Response.Complete.total_cost_usd c))
end
in
Claude.Client.run client ~handler:my_handlerFor more control, use Client.receive to get a lazy sequence:
Claude.Client.receive client
|> Seq.iter (function
| Claude.Response.Text t ->
print_endline (Claude.Response.Text.content t)
| Claude.Response.Complete c -> Printf.printf "Done!\n"
| _ -> ())Control which tools Claude can use:
let options =
Claude.Options.default
|> Claude.Options.with_allowed_tools [ "Read"; "Write"; "Bash" ]
|> Claude.Options.with_permission_mode
Claude.Permissions.Mode.Accept_editsImplement custom logic for tool approval:
let my_callback ctx =
if ctx.Claude.Permissions.tool_name = "Bash" then
Claude.Permissions.Decision.deny ~message:"Bash not allowed"
~interrupt:false
else Claude.Permissions.Decision.allow ()
let options =
Claude.Options.default
|> Claude.Options.with_permission_callback my_callbackIntercept and control tool execution with fully typed callbacks:
let hooks =
Claude.Hooks.empty
|> Claude.Hooks.on_pre_tool_use ~pattern:"Bash" (fun input ->
if
String.is_prefix ~prefix:"rm"
(input.tool_input
|> Claude.Tool_input.get_string "command"
|> Option.value ~default:"")
then Claude.Hooks.PreToolUse.deny ~reason:"Dangerous command" ()
else Claude.Hooks.PreToolUse.continue ())
let options = Claude.Options.default |> Claude.Options.with_hooks hooksThe library uses a structured exception type Err.E for all errors:
try Claude.Client.query client "Hello"
with Claude.Err.E err ->
Printf.eprintf "Error: %s\n" (Claude.Err.to_string err)Error types include:
Err.t.Cli_not_found: Claude CLI not foundErr.t.Process_error: Process execution failureErr.t.Protocol_error: JSON/protocol parsing errorErr.t.Timeout: Operation timed outErr.t.Permission_denied: Tool permission deniedErr.t.Hook_error: Hook callback errorThe library uses the Logs library for structured logging. Each module has its own log source allowing fine-grained control:
Logs.Src.set_level Claude.Client.src (Some Logs.Debug);
Logs.Src.set_level Claude.Transport.src (Some Logs.Info)module Err : sig ... endError handling with structured exception type.
module Client : sig ... endHigh-level client interface for Claude interactions.
module Options : sig ... endConfiguration options for Claude sessions.
module Response : sig ... endHigh-level response events from Claude.
module Handler : sig ... endObject-oriented response handler with sensible defaults.
module Tool_input : sig ... endOpaque tool input with typed accessors.
module Content_block : sig ... endContent blocks for messages (text, tool use, tool results, thinking).
module Message : sig ... endMessages exchanged with Claude (user, assistant, system, result).
module Permissions : sig ... endPermission system for tool invocations.
module Hooks : sig ... endFully typed hook callbacks for event interception.
module Server_info : sig ... endServer capabilities and metadata.
module Model : sig ... endClaude AI model identifiers.
module Structured_output : sig ... endStructured output configuration using JSON Schema.
These modules enable custom tool definitions that run in-process via MCP (Model Context Protocol). Unlike built-in tools which Claude CLI handles internally, custom tools are executed by your application.
let greet =
Claude.Tool.create ~name:"greet" ~description:"Greet a user"
~input_schema:
(Claude.Tool.schema_object
[ ("name", Claude.Tool.schema_string) ]
~required:[ "name" ])
~handler:(fun args ->
match Claude.Tool_input.get_string args "name" with
| Some name -> Ok (Claude.Tool.text_result ("Hello, " ^ name ^ "!"))
| None -> Error "Missing name")
let server = Claude.Mcp_server.create ~name:"my-tools" ~tools:[ greet ] ()
let options =
Claude.Options.default
|> Claude.Options.with_mcp_server ~name:"tools" server
|> Claude.Options.with_allowed_tools [ "mcp__tools__greet" ]module Tool : sig ... endCustom tool definitions for MCP servers.
module Mcp_server : sig ... endIn-process MCP servers for custom tools.
module Transport : sig ... endLow-level transport layer for CLI communication.
The Proto module provides direct access to wire-format types and JSON codecs. Use this for advanced scenarios like custom transports or debugging.
Most users should use the high-level types above instead.
module Proto = ProtoWire-format types and JSON codecs.