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
Calleryour terminal, a script, a CI runner
maelys-wardencomposes, relays, supervises
Confined workloadyour command, inside the boundary Files you namedemit-mir, receipt, network-receipts
- Caller → maelys-warden (stdin, signals)
- maelys-warden → Caller (stdout, stderr)
- maelys-warden → Confined workload (fd 0, shutdown)
- Confined workload → maelys-warden (fd 1, fd 2)
- maelys-warden → Files you named (evidence)
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:
printf 'hello\n' | maelys-warden run --ro -- /bin/cathelloIt 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:
maelys-warden run --ro --receipt /tmp/run.json -- jq -c . data.json | consumerNaming 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.
--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
- --env TOKEN → Workload environment
- --env TOKEN=s3cr3t → Workload environment
- --env TOKEN=s3cr3t → Warden argv
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.
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 = diagnosticsTwo 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.