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.
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 ().
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
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.
|||,
}]
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.
Cap.ml borrows just the capability idea, independent of eiopolysemy, fused-effects, effectful) can achieve similar fine-grained tracking, at the cost of extra machinery on top of plain IO
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.
Cap.ml library and this presentationocaml-caps is written by Yoann Padioleau, inspired by eio's capability idea.
Contact: yoann.padioleau@gmail.com