OCaml Claude SDK using Eio and Jsont
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6open Eio.Std
7
8let src = Logs.Src.create "hooks_example" ~doc:"Hooks example"
9
10module Log = (val Logs.src_log src : Logs.LOG)
11
12(* Example 1: Block dangerous bash commands *)
13let block_dangerous_bash input =
14 if input.Claude.Hooks.PreToolUse.tool_name = "Bash" then
15 match
16 Claude.Tool_input.get_string input.Claude.Hooks.PreToolUse.tool_input
17 "command"
18 with
19 | Some command ->
20 if String.length command >= 6 && String.sub command 0 6 = "rm -rf" then begin
21 Log.app (fun m -> m "🚫 Blocked dangerous command: %s" command);
22 Claude.Hooks.PreToolUse.deny
23 ~reason:"Command contains dangerous 'rm -rf' pattern" ()
24 end
25 else Claude.Hooks.PreToolUse.continue ()
26 | _ -> Claude.Hooks.PreToolUse.continue ()
27 else Claude.Hooks.PreToolUse.continue ()
28
29(* Example 2: Log all tool usage *)
30let log_tool_usage input =
31 Log.app (fun m ->
32 m "📝 Tool %s called" input.Claude.Hooks.PreToolUse.tool_name);
33 Claude.Hooks.PreToolUse.continue ()
34
35let run_example ~sw ~env =
36 Log.app (fun m -> m "🔧 Hooks System Example");
37 Log.app (fun m -> m "====================\n");
38
39 (* Configure hooks *)
40 let hooks =
41 Claude.Hooks.empty
42 |> Claude.Hooks.on_pre_tool_use log_tool_usage
43 |> Claude.Hooks.on_pre_tool_use ~pattern:"Bash" block_dangerous_bash
44 in
45
46 let options =
47 Claude.Options.default
48 |> Claude.Options.with_model (Claude.Model.of_string "sonnet")
49 |> Claude.Options.with_hooks hooks
50 in
51
52 let client =
53 Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr
54 ~clock:env#clock ()
55 in
56
57 (* Test 1: Safe command (should work) *)
58 Log.app (fun m -> m "Test 1: Safe bash command");
59 Claude.Client.query client "Run the bash command: echo 'Hello from hooks!'";
60
61 let messages = Claude.Client.receive_all client in
62 List.iter
63 (fun resp ->
64 match resp with
65 | Claude.Response.Text text ->
66 let content = Claude.Response.Text.content text in
67 if String.length content > 0 then
68 Log.app (fun m -> m "Claude: %s" content)
69 | Claude.Response.Complete _ -> Log.app (fun m -> m "✅ Test 1 complete\n")
70 | Claude.Response.Error err ->
71 Log.err (fun m -> m "❌ Error: %s" (Claude.Response.Error.message err))
72 | _ -> ())
73 messages;
74
75 (* Test 2: Dangerous command (should be blocked) *)
76 Log.app (fun m -> m "Test 2: Dangerous bash command (should be blocked)");
77 Claude.Client.query client "Run the bash command: rm -rf /tmp/test";
78
79 let messages = Claude.Client.receive_all client in
80 List.iter
81 (fun resp ->
82 match resp with
83 | Claude.Response.Text text ->
84 let content = Claude.Response.Text.content text in
85 if String.length content > 0 then
86 Log.app (fun m -> m "Claude: %s" content)
87 | Claude.Response.Complete _ -> Log.app (fun m -> m "✅ Test 2 complete")
88 | Claude.Response.Error err ->
89 Log.err (fun m -> m "❌ Error: %s" (Claude.Response.Error.message err))
90 | _ -> ())
91 messages;
92
93 Log.app (fun m -> m "\n====================");
94 Log.app (fun m -> m "✨ Example complete!")
95
96let main ~env = Switch.run @@ fun sw -> run_example ~sw ~env
97
98(* Command-line interface *)
99open Cmdliner
100
101let main_term env =
102 let setup_log style_renderer level =
103 Fmt_tty.setup_std_outputs ?style_renderer ();
104 Logs.set_level level;
105 Logs.set_reporter (Logs_fmt.reporter ());
106 if level = None then Logs.set_level (Some Logs.App);
107 match level with
108 | Some Logs.Info | Some Logs.Debug ->
109 Logs.Src.set_level Claude.Client.src (Some Logs.Info)
110 | _ -> ()
111 in
112 let run style level =
113 setup_log style level;
114 main ~env
115 in
116 Term.(const run $ Fmt_cli.style_renderer () $ Logs_cli.level ())
117
118let cmd env =
119 let doc = "Demonstrate Claude's hooks system" in
120 let info = Cmd.info "hooks_example" ~version:"1.0" ~doc in
121 Cmd.v info (main_term env)
122
123let () = Eio_main.run @@ fun env -> exit (Cmd.eval (cmd env))