Clone me on GitHub

ocaml-caps

A capability type system for OCaml — a poor man's effect system using an object system

Motivations · Capability Types · Capability Rules · Experience Report · Related Work · The Talk · Source Code

Cap.ml is a small library providing capability types to use in a disciplined manner the UNIX system calls behind standard library functions such as Sys.command, Unix.fork, or even Stdlib.open_in. When combined with the use of the Semgrep tool and capability rules, it can offer stronger guarantees on a codebase.


Motivations

If you look at the interface of a module such as:

val foo: int -> float

what can you say about this function? Can it do dangerous things? The OCaml type system offers really no guarantee here. foo() could go through your filesystem and delete all your files, execute any program, make network requests, or do any system call really. You cannot reason locally about a module.

Haskell knows this and requires the use of the IO monad to perform actions. But Haskell is another language and uses coarse granularity: a function making a network request or printing on stdout will have a similar signature, IO ().

Capability Types

Inspired by the eio library, Cap.ml generalizes the idea with plain OCaml object types — no OCaml extension needed. Here is an example:

val bar: < Cap.fork; Cap.network; Cap.stdout; Cap.random; .. > -> int -> float

The type of bar() above indicates that it requires several capabilities (Cap.fork, Cap.network, etc.) to run. Those capabilities are abstract types defined in Cap.mli and cannot be forged:

module Process : sig
  type fork (* abstract *)
  type exit (* abstract *)
  type chdir (* abstract *)
  ...
end
type fork = < fork : Process.fork >

The only function in Cap.mli that can produce values of those abstract types is main():

type all_caps = < fork; network; random; stdout; stdin; exit; chdir; ... >
val main : (all_caps -> 'a) -> 'a

main() takes a callback, dynamically checks it has not already been called, and calls the callback with an object that has all the capabilities — the "powerbox". This callback can then downcast this object and pass a subset of those capabilities to further functions. The only way to call bar() is to obtain the required capabilities by being called directly or indirectly from Cap.main() along a code path that threads them through; the OCaml type system enforces this.

Capability types are regular (abstract) OCaml types, combined together thanks to the object/row type system of OCaml. Cap.ml also contains a set of modules wrapping many standard library functions in capability-aware versions, for example:

let fork (caps : < Cap.fork; ..>) =
  let _ = caps#fork in
  Unix.fork
let chdir (caps : < Cap.chdir; ..>) =
  let _ = caps#chdir in
  Unix.chdir

Capability Rules

Of course, Cap.ml alone does not prevent programmers from calling directly the "unsafe" variant Unix.fork instead of CapUnix.fork, which is why it must be coupled with an external tool, Semgrep, to enforce the use of the safe variants. Here is an excerpt of a Semgrep configuration file forbidding the use of Unix.fork through a capability rule:

local cap_rules = [{
    id: 'use-caps',
    match: { any: [ 'Unix.fork', 'Unix.wait', ... ] },
    languages: ['ocaml'],
    severity: 'ERROR',
    message: |||
       Do not use Sys.xxx or Unix.xxx. Use CapSys or CapUnix and
       capabilities for dangerous functions.
    |||,
  }]

Experience Report

The approach was rolled out on four real codebases, between 20k and 100k lines each:

Each time, between 50 and 500 capability annotations plus a dozen Semgrep rules were enough to enforce capabilities across the whole codebase — most code turns out to be "pure". Migration usually took just a few hours, and could be done incrementally.

On the Semgrep codebase, the capability rules discovered, deep in the call stack, the use of the external program uname via Unix.open_process_in (also a performance bottleneck), added by a junior programmer that had escaped code review. Once fully migrated, the OCaml type system prevented quick-and-dirty hacks: using a dangerous function now required modifying the whole call chain to pass the necessary capability, which is easy to spot during code review.

AI coding agents have no trouble learning the capability discipline and readily produce code using capability types. Thanks to the stronger types, a reviewer can focus on the interfaces and trust, thanks to the OCaml type system, that AI-generated code in the implementation does not call dangerous functions — or if it does, the interface says so.

The Talk

A Capability Type System for OCaml — slides presented at the OCaml Workshop 2026, covering the motivations, a tour of the Cap.ml library, the experience report above, pros and cons, and related work in more detail. Here is the Youtube video of the talk: link.


Source Code


Credits

ocaml-caps is written by Yoann Padioleau, inspired by eio's capability idea.

Contact: yoann.padioleau@gmail.com