AoAH Day 4: Going recursive with Claudeio for Claude / Dec 2025
By this point, I've got three useful libraries and my use of Claude is getting better. So naturally I want to automate my invocations of the claude CLI, but I hit a roadblock: there are no OCaml SDK bindings! However, there appear to be SDKs in Python, Go and many others. So today will involve having a stab at generating Claude OCaml bindings using Eio, so I can use Claude to write more OCaml!
Approach
I prodded around the Python and noted that the communications protocol between the SDK and the CLI is JSON-RPC. I'd noticed when hacking with jsont that it includes a json-rpc codec, so adopting the same approach as I did with ocaml-jsonfeed seems reasonable: code up the core protocol using JSONt codecs, and then handle serialisation and process coordination using Eio.
For context to the agent, I gave it the Python and Go Claude SDKs to digest what the actual Claude protocol involves, and then all my previous OCaml libraries and the sources to jsont and Eio (i.e. the lessons learnt from the previous couple of days with xdge and jsonfeed).
One important prompt was to instruct it to first generate a claude.proto subpackage that only has jsont codecs and OCaml types, and then to use that package in the coordination layer with Eio. This avoids mixing up concerns in one giant module, as an unprompted Claude is prone to do.
Tests
Using jsont at the codec layer made all the difference, since I could get the model to debug the wire-level messages independently of the transport layer. In fact, I left the agent running in a loop where it looked at the error outputs from its own regression tests (against a live Claude instance) and then proceeded to fix them. This was only possible because of the excellent error instructions from jsont. For example, with the structured output test I got:
structured_output_demo.exe: [ERROR] Failed to decode incoming message: Missing member tool_name in Rule object
File "-", line 1, characters 451-515:
File "-", line 1, characters 451-515: at index 0 of
File "-", line 1, characters 450-515: array<Rule object>
File "-": in member rules of
File "-", line 1, characters 423-515: Update object
File "-", line 1, characters 423-515: at index 0 of
File "-", line 1, characters 422-515: array<Update object>
File "-": in member permission_suggestions of
File "-", line 1, characters 88-515: Permission object
File "-": in member request of
File "-", line 1, characters 0-515: ControlRequest object
Line: {"type":"control_request","request_id":"055cb59c-2f9f-457d-8c98-0a2c5a48c577","request":{"subtype":"can_use_tool","tool_name":"Bash","input":{"command":"find /Users/avsm/src/git/knot -type f -name \"*.ml\" -o -name \"*.mli\" -o -name \"*.md\" -o -name \"*.html\" -o -name \"*.go\" -o -name \"dune\" -o -name \"dune-project\" | head -100","description":"List all relevant files in the repository"},"permission_suggestions":[{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//Users/avsm/src/git/knot/**"}],"behavior":"allow","destination":"session"}],"tool_use_id":"toolu_011w6XYAbALBytxMaLxtnBGd","agent_id":"05bf9384-6c4b-4edd-898f-962d945ff724"}}
This was enough information for Claude to pick up the problem and address it in the codec:
Claude: I found the issue! The Rule decoder in proto/permissions.ml is expecting
snake_case field names (tool_name, rule_content) but the Claude CLI is sending
camelCase field names (toolName, ruleContent). The rest of the permission
system already uses camelCase consistently.
Results
I did find a lot of breakage when using different versions of the upstream Claude SDKs. For example, permission handling is just...broken... in some versions, but they seem to quite quickly push changes. I suspect they might be using a bit too much bleeding edge Claude in developing Claude!

However, their breakage did exercise my agent quite nicely into switching between the Python and Go SDKs to come up with a good answer, and also highlighted why agentic coding is so different from one-shot coding LLMs. It's pretty crazy seeing an agent dynamically introspect itself to come up with the architecture I specified, against a live service!
Reflection
It's now quite convenient to have a Claude OCaml wrapper, but I stopped short of making a really nice Eio interface as the upstream project is moving so quickly.
I'd like to eventually use this as a basis for a distributed Claude to unify my local and remote Docker development, and also integrate with our local initiatives like
Onto