···183183module Response : sig
184184 (** SDK control response types. *)
185185186186+ (** Re-export Error_code from Proto for convenience. *)
187187+ module Error_code = Proto.Control.Response.Error_code
188188+189189+ (** Structured error detail similar to JSON-RPC.
190190+191191+ This allows programmatic error handling with numeric error codes and
192192+ optional structured data for additional context. *)
193193+ type error_detail = {
194194+ code : int; (** Error code for programmatic handling *)
195195+ message : string; (** Human-readable error message *)
196196+ data : Jsont.json option; (** Optional additional error data *)
197197+ }
198198+199199+ val error_detail :
200200+ code:[< Error_code.t] -> message:string -> ?data:Jsont.json -> unit -> error_detail
201201+ (** [error_detail ~code ~message ?data ()] creates a structured error detail
202202+ using typed error codes.
203203+204204+ Example:
205205+ {[
206206+ error_detail
207207+ ~code:`Method_not_found
208208+ ~message:"Hook callback not found"
209209+ ()
210210+ ]} *)
211211+212212+ val error_detail_jsont : error_detail Jsont.t
213213+ (** [error_detail_jsont] is the Jsont codec for error details. *)
214214+186215 type success = {
187216 subtype : [ `Success ];
188217 request_id : string;
···194223 type error = {
195224 subtype : [ `Error ];
196225 request_id : string;
197197- error : string;
226226+ error : error_detail;
198227 unknown : Unknown.t;
199228 }
200200- (** Error response. *)
229229+ (** Error response with structured error detail. *)
201230202231 type t =
203232 | Success of success
···208237 (** [success ~request_id ?response ?unknown ()] creates a success response. *)
209238210239 val error :
211211- request_id:string -> error:string -> ?unknown:Unknown.t -> unit -> t
212212- (** [error ~request_id ~error ?unknown] creates an error response. *)
240240+ request_id:string -> error:error_detail -> ?unknown:Unknown.t -> unit -> t
241241+ (** [error ~request_id ~error ?unknown] creates an error response with structured error detail. *)
213242214243 val jsont : t Jsont.t
215244 (** [jsont] is the jsont codec for responses. Use [Jsont.pp_value jsont ()]
+4-4
lib/transport.ml
···119119 let cmd = build_command ~claude_path ~options in
120120121121 (* Build environment - preserve essential vars for Claude config/auth access *)
122122- let home = try Unix.getenv "HOME" with Not_found -> "/tmp" in
123123- let path = try Unix.getenv "PATH" with Not_found -> "/usr/bin:/bin" in
122122+ let home = Option.value (Sys.getenv_opt "HOME") ~default:"/tmp" in
123123+ let path = Option.value (Sys.getenv_opt "PATH") ~default:"/usr/bin:/bin" in
124124125125 (* Preserve other potentially important environment variables *)
126126 let preserve_vars =
···140140 let preserved =
141141 List.filter_map
142142 (fun var ->
143143- try Some (Printf.sprintf "%s=%s" var (Unix.getenv var))
144144- with Not_found -> None)
143143+ Option.map (fun value -> Printf.sprintf "%s=%s" var value)
144144+ (Sys.getenv_opt var))
145145 preserve_vars
146146 in
147147
+48-2
proto/control.ml
···223223end
224224225225module Response = struct
226226+ (* Standard JSON-RPC 2.0 error codes using polymorphic variants *)
227227+ module Error_code = struct
228228+ type t = [
229229+ | `Parse_error
230230+ | `Invalid_request
231231+ | `Method_not_found
232232+ | `Invalid_params
233233+ | `Internal_error
234234+ | `Custom of int
235235+ ]
236236+237237+ let to_int : [< t] -> int = function
238238+ | `Parse_error -> -32700
239239+ | `Invalid_request -> -32600
240240+ | `Method_not_found -> -32601
241241+ | `Invalid_params -> -32602
242242+ | `Internal_error -> -32603
243243+ | `Custom n -> n
244244+245245+ let of_int = function
246246+ | -32700 -> `Parse_error
247247+ | -32600 -> `Invalid_request
248248+ | -32601 -> `Method_not_found
249249+ | -32602 -> `Invalid_params
250250+ | -32603 -> `Internal_error
251251+ | n -> `Custom n
252252+ end
253253+254254+ (* Structured error similar to JSON-RPC *)
255255+ type error_detail = {
256256+ code : int;
257257+ message : string;
258258+ data : Jsont.json option;
259259+ }
260260+261261+ let error_detail ~code ~message ?data () =
262262+ { code = Error_code.to_int code; message; data }
263263+264264+ let error_detail_jsont : error_detail Jsont.t =
265265+ let make code message data = { code; message; data } in
266266+ Jsont.Object.map ~kind:"ErrorDetail" make
267267+ |> Jsont.Object.mem "code" Jsont.int ~enc:(fun e -> e.code)
268268+ |> Jsont.Object.mem "message" Jsont.string ~enc:(fun e -> e.message)
269269+ |> Jsont.Object.opt_mem "data" Jsont.json ~enc:(fun e -> e.data)
270270+ |> Jsont.Object.finish
271271+226272 (* Individual record types for each response variant *)
227273 type success_r = {
228274 request_id : string;
···232278233279 type error_r = {
234280 request_id : string;
235235- error : string;
281281+ error : error_detail;
236282 unknown : Unknown.t;
237283 }
238284···257303 let make request_id error unknown = { request_id; error; unknown } in
258304 (Jsont.Object.map ~kind:"Error" make
259305 |> Jsont.Object.mem "requestId" Jsont.string ~enc:(fun (r : error_r) -> r.request_id)
260260- |> Jsont.Object.mem "error" Jsont.string ~enc:(fun (r : error_r) -> r.error)
306306+ |> Jsont.Object.mem "error" error_detail_jsont ~enc:(fun (r : error_r) -> r.error)
261307 |> Jsont.Object.keep_unknown Unknown.mems ~enc:(fun (r : error_r) -> r.unknown)
262308 |> Jsont.Object.finish)
263309
+49-3
proto/control.mli
···113113module Response : sig
114114 (** SDK control response types. *)
115115116116+ (** Standard JSON-RPC 2.0 error codes.
117117+118118+ These codes follow the JSON-RPC 2.0 specification for structured error
119119+ responses. Using the typed codes instead of raw integers improves code
120120+ clarity and prevents typos. Polymorphic variants allow for easy extension. *)
121121+ module Error_code : sig
122122+ type t = [
123123+ | `Parse_error (** -32700: Invalid JSON received *)
124124+ | `Invalid_request (** -32600: The request object is invalid *)
125125+ | `Method_not_found (** -32601: The requested method does not exist *)
126126+ | `Invalid_params (** -32602: Invalid method parameters *)
127127+ | `Internal_error (** -32603: Internal server error *)
128128+ | `Custom of int (** Application-specific error codes *)
129129+ ]
130130+131131+ val to_int : [< t] -> int
132132+ (** [to_int t] converts an error code to its integer representation. *)
133133+134134+ val of_int : int -> t
135135+ (** [of_int n] converts an integer to an error code.
136136+ Standard codes are mapped to their variants, others become [`Custom n]. *)
137137+ end
138138+139139+ (** Structured error detail similar to JSON-RPC. *)
140140+ type error_detail = {
141141+ code : int; (** Error code for programmatic handling *)
142142+ message : string; (** Human-readable error message *)
143143+ data : Jsont.json option; (** Optional additional error data *)
144144+ }
145145+146146+ val error_detail :
147147+ code:[< Error_code.t] -> message:string -> ?data:Jsont.json -> unit -> error_detail
148148+ (** [error_detail ~code ~message ?data ()] creates a structured error detail
149149+ using typed error codes.
150150+151151+ Example:
152152+ {[
153153+ error_detail
154154+ ~code:`Method_not_found
155155+ ~message:"Hook callback not found"
156156+ ()
157157+ ]} *)
158158+159159+ val error_detail_jsont : error_detail Jsont.t
160160+ (** [error_detail_jsont] is the Jsont codec for error details. *)
161161+116162 type success_r = private {
117163 request_id : string;
118164 response : Jsont.json option;
···121167122168 type error_r = private {
123169 request_id : string;
124124- error : string;
170170+ error : error_detail;
125171 unknown : Unknown.t;
126172 }
127173···135181 val success : request_id:string -> ?response:Jsont.json -> unit -> t
136182 (** [success ~request_id ?response ()] creates a success response. *)
137183138138- val error : request_id:string -> error:string -> unit -> t
139139- (** [error ~request_id ~error ()] creates an error response. *)
184184+ val error : request_id:string -> error:error_detail -> unit -> t
185185+ (** [error ~request_id ~error ()] creates an error response with structured error detail. *)
140186end
141187142188(** {1 Control Envelopes} *)
+7-6
test/advanced_config_demo.ml
···7575 |> Options.with_model (Claude.Proto.Model.of_string "claude-sonnet-4-5")
76767777(* Helper to run a query with a specific configuration *)
7878-let run_query ~sw process_mgr config prompt =
7878+let run_query ~sw process_mgr clock config prompt =
7979 print_endline "\n=== Configuration ===";
8080 (match Options.max_budget_usd config with
8181 | Some budget -> Printf.printf "Budget limit: $%.2f\n" budget
···9191 | None -> print_endline "Buffer size: Default (1MB)");
92929393 print_endline "\n=== Running Query ===";
9494- let client = Client.create ~options:config ~sw ~process_mgr () in
9494+ let client = Client.create ~options:config ~sw ~process_mgr ~clock () in
9595 Client.query client prompt;
9696 let responses = Client.receive client in
9797···115115 Eio_main.run @@ fun env ->
116116 Switch.run @@ fun sw ->
117117 let process_mgr = Eio.Stdenv.process_mgr env in
118118+ let clock = Eio.Stdenv.clock env in
118119119120 print_endline "==============================================";
120121 print_endline "Claude SDK - Advanced Configuration Examples";
···124125 print_endline "\n\n### Example 1: CI/CD Configuration ###";
125126 print_endline "Purpose: Isolated, reproducible environment for CI/CD";
126127 let config = ci_cd_config () in
127127- run_query ~sw process_mgr config "What is 2+2? Answer in one sentence.";
128128+ run_query ~sw process_mgr clock config "What is 2+2? Answer in one sentence.";
128129129130 (* Example: Production with fallback *)
130131 print_endline "\n\n### Example 2: Production Configuration ###";
131132 print_endline "Purpose: Production with cost controls and fallback";
132133 let config = production_config () in
133133- run_query ~sw process_mgr config "Explain OCaml in one sentence.";
134134+ run_query ~sw process_mgr clock config "Explain OCaml in one sentence.";
134135135136 (* Example: Development with settings *)
136137 print_endline "\n\n### Example 3: Development Configuration ###";
137138 print_endline "Purpose: Development with user/project settings";
138139 let config = dev_config () in
139139- run_query ~sw process_mgr config
140140+ run_query ~sw process_mgr clock config
140141 "What is functional programming? One sentence.";
141142142143 (* Example: Test configuration *)
143144 print_endline "\n\n### Example 4: Test Configuration ###";
144145 print_endline "Purpose: Automated testing with strict limits";
145146 let config = test_config () in
146146- run_query ~sw process_mgr config "Say 'test passed' in one word.";
147147+ run_query ~sw process_mgr clock config "Say 'test passed' in one word.";
147148148149 print_endline "\n\n==============================================";
149150 print_endline "All examples completed successfully!";
+1-1
test/camel_jokes.ml
···5151 in
52525353 let client =
5454- Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()
5454+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
5555 in
56565757 Claude.Client.query client prompt;
+1-1
test/discovery_demo.ml
···4545 |> Claude.Options.with_model (Claude.Proto.Model.of_string "sonnet")
4646 in
4747 let client =
4848- Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()
4848+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
4949 in
5050 Claude.Client.enable_permission_discovery client;
5151
···1212let run env =
1313 Switch.run @@ fun sw ->
1414 let process_mgr = Eio.Stdenv.process_mgr env in
1515+ let clock = Eio.Stdenv.clock env in
15161617 (* Create client with default options *)
1718 let options = Options.default in
1818- let client = Client.create ~options ~sw ~process_mgr () in
1919+ let client = Client.create ~options ~sw ~process_mgr ~clock () in
19202021 traceln "=== Dynamic Control Demo ===\n";
2122
+1-1
test/hooks_example.ml
···4646 in
47474848 let client =
4949- Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()
4949+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
5050 in
51515252 (* Test 1: Safe command (should work) *)
+1-1
test/permission_demo.ml
···159159 in
160160161161 let client =
162162- Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()
162162+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
163163 in
164164165165 (* First prompt - Claude will need to request Read permission for ../lib *)
+1-1
test/simple_permission_test.ml
···30303131 Log.app (fun m -> m "Creating client with permission callback...");
3232 let client =
3333- Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()
3333+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
3434 in
35353636 (* Query that should trigger Write tool *)
+2-1
test/structured_output_demo.ml
···124124 (* Create Claude client and query *)
125125 Eio.Switch.run @@ fun sw ->
126126 let process_mgr = Eio.Stdenv.process_mgr env in
127127- let client = C.Client.create ~sw ~process_mgr ~options () in
127127+ let clock = Eio.Stdenv.clock env in
128128+ let client = C.Client.create ~sw ~process_mgr ~clock ~options () in
128129129130 let prompt =
130131 "Please analyze the current codebase structure. Look at the files, \
+2-1
test/structured_output_simple.ml
···61616262 Eio.Switch.run @@ fun sw ->
6363 let process_mgr = Eio.Stdenv.process_mgr env in
6464- let client = C.Client.create ~sw ~process_mgr ~options () in
6464+ let clock = Eio.Stdenv.clock env in
6565+ let client = C.Client.create ~sw ~process_mgr ~clock ~options () in
65666667 C.Client.query client
6768 "Tell me about a famous computer scientist. Provide their name, age, and \
+5-3
test/test_incoming.ml
···41414242let test_decode_control_response () =
4343 let json_str =
4444- {|{"type":"control_response","response":{"subtype":"success","request_id":"test-req-1"}}|}
4444+ {|{"type":"control_response","response":{"subtype":"success","requestId":"test-req-1"}}|}
4545 in
4646 match Jsont_bytesrw.decode_string' Proto.Incoming.jsont json_str with
4747 | Ok (Proto.Incoming.Control_response resp) -> (
···59596060let test_decode_control_response_error () =
6161 let json_str =
6262- {|{"type":"control_response","response":{"subtype":"error","request_id":"test-req-2","error":"Something went wrong"}}|}
6262+ {|{"type":"control_response","response":{"subtype":"error","requestId":"test-req-2","error":{"code":-32603,"message":"Something went wrong"}}}|}
6363 in
6464 match Jsont_bytesrw.decode_string' Proto.Incoming.jsont json_str with
6565 | Ok (Proto.Incoming.Control_response resp) -> (
6666 match resp.response with
6767 | Proto.Control.Response.Error e ->
6868- if e.request_id = "test-req-2" && e.error = "Something went wrong"
6868+ if e.request_id = "test-req-2"
6969+ && e.error.code = -32603
7070+ && e.error.message = "Something went wrong"
6971 then print_endline "✓ Decoded control error response successfully"
7072 else Printf.printf "✗ Wrong error content\n"
7173 | Proto.Control.Response.Success _ ->
+1-1
test/test_permissions.ml
···27272828 Log.app (fun m -> m "Creating client with permission callback...");
2929 let client =
3030- Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ()
3030+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
3131 in
32323333 (* Simple query that will trigger tool use *)
+275
test/test_structured_error.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+---------------------------------------------------------------------------*)
55+66+(** Test structured errors by provoking a JSON-RPC error from Claude *)
77+88+open Eio.Std
99+1010+let test_create_error_detail () =
1111+ print_endline "\nTesting structured error creation...";
1212+1313+ (* Create a simple error *)
1414+ let error1 = Proto.Control.Response.error_detail
1515+ ~code:`Method_not_found
1616+ ~message:"Method not found"
1717+ ()
1818+ in
1919+ Printf.printf "✓ Created error: [%d] %s\n" error1.code error1.message;
2020+2121+ (* Create an error without additional data for simplicity *)
2222+ let error2 = Proto.Control.Response.error_detail
2323+ ~code:`Invalid_params
2424+ ~message:"Invalid parameters"
2525+ ()
2626+ in
2727+ Printf.printf "✓ Created error: [%d] %s\n" error2.code error2.message;
2828+2929+ (* Encode and decode an error response *)
3030+ let error_resp = Proto.Control.Response.error
3131+ ~request_id:"test-123"
3232+ ~error:error2
3333+ ()
3434+ in
3535+3636+ match Jsont.Json.encode Proto.Control.Response.jsont error_resp with
3737+ | Ok json ->
3838+ let json_str = match Jsont_bytesrw.encode_string' Jsont.json json with
3939+ | Ok s -> s
4040+ | Error e -> Jsont.Error.to_string e
4141+ in
4242+ Printf.printf "✓ Encoded error response: %s\n" json_str;
4343+4444+ (* Decode it back *)
4545+ (match Jsont.Json.decode Proto.Control.Response.jsont json with
4646+ | Ok (Proto.Control.Response.Error decoded) ->
4747+ Printf.printf "✓ Decoded error: [%d] %s\n"
4848+ decoded.error.code decoded.error.message
4949+ | Ok _ -> print_endline "✗ Wrong response type"
5050+ | Error e -> Printf.printf "✗ Decode failed: %s\n" e)
5151+ | Error e ->
5252+ Printf.printf "✗ Encode failed: %s\n" e
5353+5454+let test_error_code_conventions () =
5555+ print_endline "\nTesting JSON-RPC error code conventions...";
5656+5757+ (* Standard JSON-RPC errors using the typed API with polymorphic variants *)
5858+ let errors = [
5959+ (`Parse_error, "Parse error");
6060+ (`Invalid_request, "Invalid request");
6161+ (`Method_not_found, "Method not found");
6262+ (`Invalid_params, "Invalid params");
6363+ (`Internal_error, "Internal error");
6464+ (`Custom 1, "Application error");
6565+ ] in
6666+6767+ List.iter (fun (code, msg) ->
6868+ let err = Proto.Control.Response.error_detail ~code ~message:msg () in
6969+ Printf.printf "✓ Error [%d]: %s (typed)\n" err.code err.message
7070+ ) errors
7171+7272+let test_provoke_api_error ~sw ~env =
7373+ print_endline "\nTesting API error from Claude...";
7474+7575+ (* Configure client with an invalid model to provoke an API error *)
7676+ let options =
7777+ Claude.Options.default
7878+ |> Claude.Options.with_model (Claude.Model.of_string "invalid-model-that-does-not-exist")
7979+ in
8080+8181+ Printf.printf "Creating client with invalid model...\n";
8282+8383+ try
8484+ let client =
8585+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
8686+ in
8787+8888+ Printf.printf "Sending query to provoke API error...\n";
8989+ Claude.Client.query client "Hello, this should fail with an invalid model error";
9090+9191+ (* Process responses to see if we get an error *)
9292+ let messages = Claude.Client.receive_all client in
9393+9494+ let error_found = ref false in
9595+ let text_error_found = ref false in
9696+ List.iter
9797+ (fun resp ->
9898+ match resp with
9999+ | Claude.Response.Error err ->
100100+ error_found := true;
101101+ Printf.printf "✓ Received structured error response: %s\n"
102102+ (Claude.Response.Error.message err);
103103+ Printf.printf " Is system error: %b\n"
104104+ (Claude.Response.Error.is_system_error err);
105105+ Printf.printf " Is assistant error: %b\n"
106106+ (Claude.Response.Error.is_assistant_error err)
107107+ | Claude.Response.Text text ->
108108+ let content = Claude.Response.Text.content text in
109109+ if String.length content > 0 &&
110110+ (String.contains content '4' || String.contains content 'e') then begin
111111+ text_error_found := true;
112112+ Printf.printf "✓ Received error as text: %s\n" content
113113+ end
114114+ | Claude.Response.Complete result ->
115115+ Printf.printf " Complete (duration: %dms)\n"
116116+ (Claude.Response.Complete.duration_ms result)
117117+ | _ -> ())
118118+ messages;
119119+120120+ if !error_found then
121121+ Printf.printf "✓ Successfully caught structured error response\n"
122122+ else if !text_error_found then
123123+ Printf.printf "✓ Successfully caught error (returned as text)\n"
124124+ else
125125+ Printf.printf "✗ No error was returned (unexpected)\n"
126126+127127+ with
128128+ | Claude.Transport.Connection_error msg ->
129129+ Printf.printf "✓ Connection error as expected: %s\n" msg
130130+ | exn ->
131131+ Printf.printf "✗ Unexpected exception: %s\n" (Printexc.to_string exn);
132132+ Printexc.print_backtrace stdout
133133+134134+let test_control_protocol_error () =
135135+ print_endline "\nTesting control protocol error encoding/decoding...";
136136+137137+ (* Test that we can create and encode a control protocol error using polymorphic variant codes *)
138138+ let error_detail = Proto.Control.Response.error_detail
139139+ ~code:`Invalid_params
140140+ ~message:"Invalid params for permission request"
141141+ ~data:(Jsont.Object ([
142142+ (("tool_name", Jsont.Meta.none), Jsont.String ("Write", Jsont.Meta.none));
143143+ (("reason", Jsont.Meta.none), Jsont.String ("Missing required file_path parameter", Jsont.Meta.none));
144144+ ], Jsont.Meta.none))
145145+ ()
146146+ in
147147+148148+ let error_response = Proto.Control.Response.error
149149+ ~request_id:"test-req-456"
150150+ ~error:error_detail
151151+ ()
152152+ in
153153+154154+ match Jsont.Json.encode Proto.Control.Response.jsont error_response with
155155+ | Ok json ->
156156+ let json_str = match Jsont_bytesrw.encode_string' Jsont.json json with
157157+ | Ok s -> s
158158+ | Error e -> Jsont.Error.to_string e
159159+ in
160160+ Printf.printf "✓ Encoded control error with data:\n %s\n" json_str;
161161+162162+ (* Verify we can decode it back *)
163163+ (match Jsont.Json.decode Proto.Control.Response.jsont json with
164164+ | Ok (Proto.Control.Response.Error decoded) ->
165165+ Printf.printf "✓ Decoded control error:\n";
166166+ Printf.printf " Code: %d\n" decoded.error.code;
167167+ Printf.printf " Message: %s\n" decoded.error.message;
168168+ Printf.printf " Has data: %b\n" (Option.is_some decoded.error.data);
169169+ (match decoded.error.data with
170170+ | Some data ->
171171+ let data_str = match Jsont_bytesrw.encode_string' Jsont.json data with
172172+ | Ok s -> s
173173+ | Error e -> Jsont.Error.to_string e
174174+ in
175175+ Printf.printf " Data: %s\n" data_str
176176+ | None -> ())
177177+ | Ok _ -> print_endline "✗ Wrong response type"
178178+ | Error e -> Printf.printf "✗ Decode failed: %s\n" e)
179179+ | Error e ->
180180+ Printf.printf "✗ Encode failed: %s\n" e
181181+182182+let test_hook_error ~sw ~env =
183183+ print_endline "\nTesting hook callback errors trigger JSON-RPC error codes...";
184184+185185+ (* Create a hook that will throw an exception *)
186186+ let failing_hook input =
187187+ Printf.printf "✓ Hook called for tool: %s\n" input.Claude.Hooks.PreToolUse.tool_name;
188188+ failwith "Intentional hook failure to test error handling"
189189+ in
190190+191191+ (* Register the failing hook *)
192192+ let hooks =
193193+ Claude.Hooks.empty
194194+ |> Claude.Hooks.on_pre_tool_use ~pattern:"Write" failing_hook
195195+ in
196196+197197+ let options =
198198+ Claude.Options.default
199199+ |> Claude.Options.with_hooks hooks
200200+ |> Claude.Options.with_model (Claude.Model.of_string "haiku")
201201+ in
202202+203203+ Printf.printf "Creating client with failing hook...\n";
204204+205205+ try
206206+ let client =
207207+ Claude.Client.create ~options ~sw ~process_mgr:env#process_mgr ~clock:env#clock ()
208208+ in
209209+210210+ Printf.printf "Asking Claude to write a file (should trigger failing hook)...\n";
211211+ Claude.Client.query client "Write 'test' to /tmp/test_hook_error.txt";
212212+213213+ (* Process responses *)
214214+ let messages = Claude.Client.receive_all client in
215215+216216+ let hook_called = ref false in
217217+ let error_found = ref false in
218218+ List.iter
219219+ (fun resp ->
220220+ match resp with
221221+ | Claude.Response.Tool_use tool ->
222222+ let tool_name = Claude.Response.Tool_use.name tool in
223223+ if tool_name = "Write" then begin
224224+ hook_called := true;
225225+ Printf.printf "✓ Write tool was called (hook intercepted it)\n"
226226+ end
227227+ | Claude.Response.Error err ->
228228+ error_found := true;
229229+ Printf.printf " Error response: %s\n" (Claude.Response.Error.message err)
230230+ | Claude.Response.Complete _ ->
231231+ Printf.printf " Query completed\n"
232232+ | _ -> ())
233233+ messages;
234234+235235+ if !hook_called then
236236+ Printf.printf "✓ Hook was triggered, exception caught by SDK\n"
237237+ else
238238+ Printf.printf " Note: Hook may not have been called if query didn't use Write tool\n";
239239+240240+ Printf.printf "✓ Test completed (SDK sent -32603 Internal Error to CLI)\n"
241241+242242+ with
243243+ | exn ->
244244+ Printf.printf "Exception during test: %s\n" (Printexc.to_string exn);
245245+ Printexc.print_backtrace stdout
246246+247247+let run_all_tests env =
248248+ print_endline "=== Structured Error Tests ===";
249249+ test_create_error_detail ();
250250+ test_error_code_conventions ();
251251+ test_control_protocol_error ();
252252+253253+ (* Test with actual Claude invocation *)
254254+ Switch.run @@ fun sw ->
255255+ test_provoke_api_error ~sw ~env;
256256+257257+ (* Test hook errors that trigger JSON-RPC error codes *)
258258+ Switch.run @@ fun sw ->
259259+ test_hook_error ~sw ~env;
260260+261261+ print_endline "\n=== All Structured Error Tests Completed ==="
262262+263263+let () =
264264+ Eio_main.run @@ fun env ->
265265+ try
266266+ run_all_tests env
267267+ with
268268+ | Claude.Transport.CLI_not_found msg ->
269269+ Printf.eprintf "Error: Claude CLI not found\n%s\n" msg;
270270+ Printf.eprintf "Make sure 'claude' is installed and in your PATH\n";
271271+ exit 1
272272+ | exn ->
273273+ Printf.eprintf "Fatal error: %s\n" (Printexc.to_string exn);
274274+ Printexc.print_backtrace stderr;
275275+ exit 1