# AoAH Day 4: Going recursive with Claudeio for Claude

*2025-12-04 — note*


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](https://github.com/anthropics/claude-agent-sdk-python), [Go](https://github.com/anthropics/anthropic-sdk-go) and [many others](https://github.com/anthropics). So today will involve having a stab at generating [Claude OCaml bindings](https://tangled.org/anil.recoil.org/claudeio) using Eio, so I can use Claude to write more OCaml\!


## Approach

I prodded around the [Python](https://github.com/anthropics/claude-agent-sdk-python) and noted that the communications protocol between the SDK and the CLI is JSON-RPC. I'd noticed [when hacking with jsont](https://anil.recoil.org/notes/aoah-2025-2) that it includes a [json-rpc codec](https://github.com/dbuenzli/jsont/blob/main/test/json_rpc.ml), so adopting the same approach as I did with [ocaml-jsonfeed](https://tangled.org/anil.recoil.org/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\!

<figure class="image-center"><img src="/images/claude-ss-perm-1.webp" alt="I managed to get interactive OCaml callbacks to Claude working after some upstream fixes" title="I managed to get interactive OCaml callbacks to Claude working after some upstream fixes" loading="lazy" srcset="/images/claude-ss-perm-1.768.webp 768w, /images/claude-ss-perm-1.640.webp 640w, /images/claude-ss-perm-1.480.webp 480w, /images/claude-ss-perm-1.320.webp 320w, /images/claude-ss-perm-1.1920.webp 1920w, /images/claude-ss-perm-1.1600.webp 1600w, /images/claude-ss-perm-1.1440.webp 1440w, /images/claude-ss-perm-1.1280.webp 1280w, /images/claude-ss-perm-1.1024.webp 1024w"><figcaption>I managed to get interactive OCaml callbacks to Claude working after some upstream fixes</figcaption></figure>

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](https://tangled.org/anil.recoil.org/claudeio), 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 [Ryan Gibb](https://ryan.freumh.org) and his work on [package management](https://anil.recoil.org/papers/2025-hyperres) and [Patrick Ferris](https://patrick.sirref.org)'s cool new [Shelter](https://patrick.sirref.org/weekly-2025-w49/index.xml).  For now, I'm holding the fort on just doing simple OCaml invocations of Claude and not trying anything too exotic until the CLI itself settles down and stabilises.

Onto [Day 5](https://anil.recoil.org/notes/aoah-2025-5) next, where we use Claude skills for the first time\!
Synopsis: Creating OCaml bindings for the Claude API using Eio and jsont codecs by reverse-engineering the JSON-RPC protocol from Python and Go SDKs, enabling Claude to write more Claude-powered OCaml code.
Words: 745

## Related

- [2025 Advent of Agentic Humps: Building a useful O(x)Caml library every day](https://anil.recoil.org/notes/aoah-2025) (note, 2025-12-26)
- [AoAH Day 24: Tuatara, an evolving Atom aggregator that mutates](https://anil.recoil.org/notes/aoah-2025-24) (note, 2025-12-24)
- [AoAH Day 21: Complete dynamic HTML5 validation in OCaml and the browser](https://anil.recoil.org/notes/aoah-2025-21) (note, 2025-12-21)
- [AoAH Day 16: Vibesplaining JSON Pointers using OCaml/Javascript](https://anil.recoil.org/notes/aoah-2025-16) (note, 2025-12-16)
- [AoAH Day 13: Heckling an OCaml HTTP client from 50 implementations in 10 languages](https://anil.recoil.org/notes/aoah-2025-13) (note, 2025-12-13)
- [AoAH Day 10: Building a TUI for Sortal using Mosaic](https://anil.recoil.org/notes/aoah-2025-10) (note, 2025-12-10)
- [AoAH Day 5: Bytesrw Eio adapters and automating opam metadata](https://anil.recoil.org/notes/aoah-2025-5) (note, 2025-12-05)
- [AoAH Day 3: XDG filesystem paths using Eio capabilities](https://anil.recoil.org/notes/aoah-2025-3) (note, 2025-12-03)
- [AoAH Day 2: Building an OCaml JSONFeed library](https://anil.recoil.org/notes/aoah-2025-2) (note, 2025-12-02)
- [Solving Package Management via Hypergraph Dependency Resolution](https://anil.recoil.org/papers/2025-hyperres) (paper, 2025-06-01)
- [OxCaml Labs](https://anil.recoil.org/projects/oxcaml) (project, 2025-01-01)

---
Canonical: https://anil.recoil.org/notes/aoah-2025-4
Type: note
License: CC BY 4.0 <https://creativecommons.org/licenses/by/4.0/>
Tags: aoah, ocaml, agents, llms, ai
