Capabilities

A capability is a named piece of the boundary a tool runs inside: network, mount mode, browser, clipboard, PID namespace, GPU. The sandbox's default is the strict side of every one of them, and each capability is a deliberate, named relaxation.

Capabilities come from three sources, most specific first:

  1. --cap on the command line
  2. the tool's entry in kapsl.toml (or a trusted project .kapslrc)
  3. the default the image itself declares

A capability is never inferred from what a tool appears to need. If the boundary you get is wrong, the fix is to name it.

The vocabulary

The complete --cap vocabulary, in the order the binary lists it:

CapWhat it changesWho may grant it
netOutbound network. Without it the container has a network namespace with no interface — not even DNS. --port can still publish a port into it.declarable + CLI
rwThe CWD mount becomes :rw (the default is :ro).declarable + CLI
roForces the CWD mount read-only, overriding an image's own default that might be rw.CLI only
nomountNo CWD mount at all — the working directory is not readable, not just unwritable. Stricter than ro.CLI only
rwimgThe image's own filesystem is read-write. Advanced debugging: the tool can write into its own base image layers.declarable + CLI
browserA FIFO URL relay plus a $BROWSER wrapper; when the tool opens a URL, the host opens it.declarable + CLI
clipboardAn OSC 52 shim at /kapsl-clipboard.sh plus the $KAPSL_CLIPBOARD variable — the copy mechanism an editor can find.declarable + CLI
pidThe host PID namespace instead of a private one (what htop shows you).declarable + CLI
gpuGPU device passthrough via CDI. podman/docker only — a no-op on Apple's container runtime.declarable + CLI
netrawCAP_NET_RAW inside the container's own namespace: raw sockets for ping, traceroute, tcpdump.declarable + CLI
nonetForces network off, even if the tool or a composed-in tool would auto-grant it.CLI only
nobrowserForces the browser relay off.CLI only
noclipboardForces the clipboard shim off.CLI only
nestkapsl-in-kapsl: tools in this container can re-run kapsl through a brokered socket; each nested tool gets its own sandbox. Recursion stays bounded — a nested run never inherits nest.CLI only

--privileged is not a capability value; it is a separate flag that disables all capability restrictions and seccomp filtering at once.

Usage: comma- or space-separated, and the flag is repeatable.

$ kapsl --cap net,rw python script.py
$ kapsl --cap ro --cap nonet make test

What each one means, concretely

  • net — with it, the container's network namespace gets an interface and outbound works; without it, curl fails before it can try. Publishing a port (-p) is the host's side of the same wall — the host can reach in even where the container can't reach out.
  • rw / ro / nomount — the three CWD mount modes. ro is the default posture (your tree is readable, not writable); rw lets the tool write to it; nomount removes it from the container's view entirely. A tool that reads your sources but never writes them needs none of these — the read-only default already covers it.
  • browser — the tool cannot open a real browser; it writes a URL into a FIFO, and the host side opens it in your actual browser. The tool sees $BROWSER; you see the tab.
  • clipboard — mounts a shim an editor can find. OSC 52 is a sequence written to the terminal, and a tool with a terminal can write it either way. Withholding a terminal is what withholds that whole class.
  • pid — with a private PID namespace, the tool sees only its own process tree; pid swaps in the host's, which is what makes a containerized htop show your real system.
  • gpu — CDI device passthrough. This is the one that is not just a namespace flag: it is a real attack-surface expansion, which is why granting it prints a warning.
  • netraw — raw sockets, contained to the container's own namespace. This is the capability an image can declare for itself (it is image-label-driven), which is how a network tool ships with the one knob it actually needs.
  • nest — the escape hatch for tools that themselves wrap other tools. Anything running in the container can start a nested kapsl run, so it is CLI-only by design: a tool's own metadata can never grant it to itself, and the nesting depth stays bounded because a nested run cannot carry nest into the next level.

Negations and conflicts

The negation pattern is the same mechanism in reverse: a tool or a composed-in tool can auto-grant a capability, and nonet / nobrowser / noclipboard force it off anyway. Negations are mutually exclusive with their positive form, and the binary checks that at parse time:

$ kapsl --cap ro --cap rw jq '.a'
  ■ ERROR      --cap: 'ro', 'rw', and 'nomount' are mutually exclusive — pick
               one

$ kapsl --cap nonet --port 8080:80 jq '.a'
  ■ ERROR      --cap nonet conflicts with --port: a published port requires
               network access

Both exit 1 before anything runs. --cap gpu with --runtime container is likewise rejected — Apple's container runtime does not expose CDI GPU device passthrough.

What a tool may declare — and what only you may grant

The catalogue, [tools], and .kapslrc can declare a restricted set: net, rw, browser, clipboard, pid, gpu, rwimg, netraw. That is the set a signed tool can ask for, and it is checked against the field.

ro, nomount, the three negations, and nest are CLI-only. The split is the point: tightening (ro, nomount, the negations) is something you can do to any tool from the command line, and the one capability that changes what the container may start (nest) must never be grantable by a tool's own metadata. A tool can ask to be reached, to write, to show you things — it cannot ask to be a container runtime.

Which tools declare which

Two ways to find out before you run:

$ kapsl --info jq
   CAPS          ro
   SECCOMP      net-listen (no class declared)  (tier: default)

and the index, which lists each tool's declared boundary at index.kapsl.sh.

Relaxation is explicit

Every escalation prints itself. --privileged says it disables all capability restrictions and seccomp filtering; --cap gpu says it is a real attack-surface expansion; --cap nest says anything running here can start another sandbox. If a run changed the boundary, the transcript says so — see Security Model for why that is a design requirement, and Exit Codes for what happens when a run stops instead.