Start here

Command and process model

How Warden wraps one command: what it composes, how stdio and signals cross the boundary, why evidence stays out of the workload output, and the isolated fd 3 channel.

Everything the quickstart and the two tutorials do runs one command through maelys-warden. This page is about the wrapper itself: what sits between your terminal and the confined process, what crosses that boundary, and what deliberately does not.

maelys-warden is the command-line front of libmaelys-warden, the product library. It accepts a policy in any of its three forms — inline flags, a JSON document, or an already-compiled MIR file — and turns it into exactly one supervised process. Four consequences follow, and this page is those four.

What the wrapper puts around your command

ARCHITECTUREOne command, and what Warden keeps on either side of it
    • Calleryour terminal, a script, a CI runner
    • maelys-wardencomposes, relays, supervises
    • Confined workloadyour command, inside the boundary
    • Files you namedemit-mir, receipt, network-receipts

Bytes make a round trip: what you send reaches the command's own standard input, and what the command prints comes back on Warden's. Evidence takes the other exit, into the files you named, so it never appears in the workload's output.

It composes three engines and adds no fourth opinion

Sandbox policy resolution takes the roots your policy names — workspace, the executable, a private temporary directory — resolves them against a trusted host context, and produces the plan that will be enforced.

Executor is the engine that runs that plan. Its own package description is the exact one: a policy-agnostic process execution and sandbox backend library. It prepares, spawns, waits for and stops the process, and it selects which backend enforces the plan — that selection step is the planner, which compares what the plan requires against what each installed backend can guarantee, and refuses the run rather than weakening it. Executor links against maelys-sys and nothing else, so it holds no policy language of its own: the separation is a build-time fact rather than a convention.

Egress is the network enforcement engine, and it only exists in a run whose policy has mediated destinations. It authorises the connection, opens it on the workload's behalf and records it. Egress was previously called Netd before its 0.9.0 release and the identifiers written on disk still say so; receipts explains which literal strings to match on.

Warden composes those three and adds no opinion of its own about authority. Architecture covers what each one is forbidden from doing.

It relays standard input, output and error

The workload does not inherit your terminal. Executor creates three pipes, and Warden copies bytes across them in both directions: what you type or pipe in reaches the command's fd 0, and what it writes on fd 1 and fd 2 comes back on Warden's own stdout and stderr, byte for byte.

The practical effect is that a confined command behaves like the command itself. It composes in a pipeline, it can be redirected, and its exit code is Warden's exit code:

CODE
printf 'hello\n' | maelys-warden run --ro -- /bin/cat
CODE
hello

It turns your interrupt into an ordered shutdown

Warden installs handlers for SIGINT, SIGTERM and SIGHUP. Pressing Ctrl+C, or having a supervisor terminate the run, therefore does not kill the wrapper and leave the real command running behind it.

Instead the signal starts an ordered shutdown of the confined execution — the whole process tree, not only the immediate child — after which Warden waits for it to actually exit. It still writes the receipts you asked for, and its own exit code becomes 128 + signal, the ordinary Unix convention.

It keeps the evidence out of the workload's output

--emit-mir, --receipt and --network-receipts write to the paths you name, each atomically and with mode 0600. Nothing is ever merged into the stream the workload is writing on.

That separation is what makes the wrapper safe to put in front of a program whose output you parse. A run that also writes a receipt still delivers exactly what the workload produced:

CODE
maelys-warden run --ro --receipt /tmp/run.json -- jq -c . data.json | consumer

Naming an environment value without exposing it

--env is repeatable and has two spellings. The difference between them is a security one rather than a matter of taste.

--env NAME imports the value of NAME from the environment the CLI itself was started in, and writes only the name on the command line. --env NAME=VALUE is an explicit override, and the value then is part of Warden's arguments.

That matters because a process's argument vector is not private: any other process on the machine can read it, with ps and its equivalents. A token written as --env TOKEN=s3cr3t is legible there for the whole run, whereas the same token passed as --env TOKEN never appears.

ARCHITECTURETwo spellings, one destination, one extra exposure
    • --env TOKENonly the name is on the command line
    • --env TOKEN=s3cr3tthe value is on the command line
    • Workload environmentwhere both spellings arrive, through execve
    • Warden argvreadable by any local process, with ps

Both spellings deliver the same value to the workload, in its environment and never in its own argv. Only the second one also writes that value where the rest of the machine can read it.

Use NAME=VALUE for things you would not mind seeing in a process list, such as a build profile, and the bare name for everything else. The macOS tutorial shows this on a real run, where it matters most: an OCI target starts from an empty environment, so --env is the only way in.

Why the defaults are what they are

The backend. On macOS Apple Silicon it is seatbelt, on Linux it is bubblewrap. That is not a preference: those are the confining backends a base install compiles in, and the others are optional driver packages. The backend table in the quickstart lists what ships where.

The network frontend. --network-frontend auto resolves to proxy for a mediated policy, because ordinary tools — curl, git, npm — already read the standard proxy environment variables and need no modification whatsoever. It resolves to none when the policy refuses the network, since there is then nothing to deliver. fd4 is never chosen for you, and the reason is not caution: the workload has to implement the fd 4 protocol itself, so an unmodified program simply cannot use it.

The Linux relay. With the proxy frontend under Bubblewrap, the loopback relay is a small helper binary shipped in libexec — the conventional Unix directory for programs that other software executes rather than users type. Warden finds it there by itself and you supply nothing; --relay-bootstrap overrides the lookup for unusual install prefixes and is documented in the CLI reference.

Every default above describes a local target: a command already installed on your machine, confined in place as an ordinary host process. Adding --image REFERENCE@sha256:DIGEST makes the run an OCI target instead — the program comes from a digest-pinned image and runs inside a virtual machine — which has its own candidates and its own receipt schema. The macOS tutorial walks that path.

The isolated protocol channel

Everything above assumes the caller talks to the workload the ordinary way, through stdin and stdout. --io isolated-duplex is for the case where the workload speaks a protocol of its own — an MCP provider, a code runner — and mixing that protocol with human-readable output would be a bug.

Every process starts with three well-known descriptors: fd 0 for input, fd 1 for output, fd 2 for diagnostics. Descriptor 3 is simply the next one, and Executor's contract is to install there one end of a full-duplex socket pair. A child written for that contract reads its requests and writes its responses on fd 3, and Warden transports those bytes without interpreting them.

CODE
caller                      maelys-warden                  child

stdin   -- request frames --->  relay  ----------------->  fd 3
stdout  <--- response frames -  relay  <-----------------  fd 3
stderr  <--- diagnostics -----  relay  <-----------------  fd 2
                                                           fd 0 = /dev/null
                                                           fd 1 = diagnostics

Two properties make the channel trustworthy. The child's own fd 1 is redirected onto its diagnostics, so an accidental printf lands on stderr and cannot corrupt a frame. And the CLI's stdout carries nothing but the bytes read from fd 3.

Closing your end of stdin performs a half-close: Warden shuts down only the direction that was carrying your requests, and leaves the return direction open. The child can still send its last response, and the CLI drains that final frame after the child exits rather than truncating it.

This mode uses Executor's native fd 3 contract. It never uses MAELYS_PRESERVE_FDS, which is reserved for additional channels from fd 4 upward — and it is orthogonal to --network-frontend fd4, which is about how the workload obtains a connection, not about how you talk to it. The CLI reference tabulates the four combinations.

For a complete worked path — Provider Node to CLI to libmaelys-warden to Executor to runner, with its lifecycle and confinement guarantees — see the isolated Code Mode relay.