brig docs

Concepts

Runtimes

On this page

Brig does not boot a sandbox. A runtime does: hull on macOS, nerdctl on Linux. Under hull, vz, hvi and qemu are hypervisor backends, called backends below.

Platform comparison #

macOS Linux
Runtime Brig drives hull nerdctl
Projects you take on Brig and hull Brig, plus three projects it does not own: nerdctl, containerd and urunc
What boots the microVM A hull backend: hvi, vz or qemu The containerd shim io.containerd.urunc.v2 (urunc)
Binary Brig looks for on PATH hull nerdctl, then docker
Isolated network A gateway per sandbox, on the hvi backend A network per sandbox, made with nerdctl network create
Egress policy enforced on the hvi backend cannot enforce
tmpfs mounts Mounted in the running sandbox with exec -u root Passed at create time with --tmpfs
Boot bundle Fetched by hull Carried by the runtime bundle. oras fetches it on a host without that bundle.
Where the version pin lives The hull cask in brig-sh/homebrew-brig RUNTIME_VERSION in install.sh

On Linux, install.sh installs nerdctl, containerd and urunc from the runtime bundle described in Install. The nerdctl and containerd in that bundle are upstream releases. For its urunc, see Runtime requirements.

Components #

The licences were read from each repository's LICENSE file on 2026-08-26, and not checked again after that date.

Component What it does Where it comes from Licence
brig, brigd Resolves the profile, the guest home and the credentials, then drives the runtime The Brig repository Apache-2.0
hull CLI that pulls an OCI image and boots it as a microVM on Apple Silicon brig-sh/hull Apache-2.0
vz-runner Swift helper hull launches for its vz backend. It is what talks to Virtualization.framework. Same repository as hull, installed beside it Apache-2.0
hvi Separate microVM monitor for hull's hvi backend. It talks to Hypervisor.framework directly. brig-sh/hvi-vmm, a git submodule of hull, installed beside hull Apache-2.0
nerdctl Docker-compatible CLI for containerd. It is the binary Brig drives on Linux. containerd/nerdctl Apache-2.0
containerd Daemon underneath nerdctl. It holds the image store and hands each container to a shim. containerd/containerd Apache-2.0
urunc The containerd shim io.containerd.urunc.v2. It boots the container as a microVM, not as a process. urunc-dev/urunc, built by the Linux runtime bundle from the feat/unchanged_containers-exec-fixes branch Apache-2.0
cosign Verifies the signature on a guest image before it boots. Optional. Without it, verification degrades to a warning. sigstore/cosign Apache-2.0
oras Pulls the boot bundle on Linux for a genericBoot profile. Optional otherwise. oras-project/oras Apache-2.0
Boot bundle The kernel, container-initrd and the in-guest agent. They let Brig exec into an image built as an ordinary container. Published as an OCI artifact at ghcr.io/nofireai/hull-assets, one tag per guest platform. Fetched by hull on macOS. On Linux the runtime bundle carries its own kernel and initrd, and oras fetches this one only on a host without that bundle. Unconfirmed. It is signed with keyless cosign, but the repository that builds it is not public and states no licence.

nerdctl, containerd and urunc predate Brig. Brig passes them only flags that any other caller can pass.

Architecture shows how hull, hvi and the network gateway fit together on macOS.

macOS backends #

Six of the eight shipped profiles set hypervisor: hvi, network: isolated and genericBoot: true (internal/profile/specs). Each new sandbox of those profiles uses hvi and gets a separate network. This path needs the hvi binary beside hull, a working gateway, and the boot bundle.

hull accepts three values for its --hypervisor flag. Brig always passes the flag. The value is BRIG_HYPERVISOR if that is set, and otherwise the backend the profile names.

hvi vz qemu
Talks to Hypervisor.framework directly, through the hvi microVM monitor Virtualization.framework, through the vz-runner helper Hypervisor.framework, through QEMU's -accel hvf
Chosen when The profile or BRIG_HYPERVISOR names it The profile or BRIG_HYPERVISOR names it, or neither names a backend The profile or BRIG_HYPERVISOR names it
Shipped profiles Six of eight, with network: isolated Two, with network: shared None
Network gateway Yes. The only backend that runs one. No No
--network isolated Runs Refused Refused
Attached egress policy Runs Refused Refused
Graphical console (kind: gui profile) Refused Runs. The only backend that can show one. Refused

hull does not ship QEMU. Install it with brew install qemu. hull's backends page says what else each backend needs.

Note brig doctor's Hypervisor.framework available line does not report on these three backends. It is the pass string of one kern.hv_support sysctl read, which shows only whether this Mac can back a microVM. It never gates the exit status.

Other backends and custom profiles

BRIG_HYPERVISOR=vz with BRIG_NETWORK=shared moves the six hvi profiles to the vz backend. It overrides the isolation that vz cannot provide.

A custom profile with no network choice falls back to shared on vz or qemu. The NETWORK row in brig info names the reason. An explicit isolated is still refused.

The boundary #

Brig decides the following:

  • which image, and whether its signature verified (internal/verify)
  • which host directory is the guest home
  • which credentials are resolved, which are denied for billing safety, and which are handed in by name and not by value
  • the sandbox name, memory, CPU count, network mode, pull policy, root filesystem type, hypervisor backend and shared directories
  • on macOS, the kernel and initrd paths for an image that carries no kernel, and the gateway address each sandbox takes

The runtime decides the following:

  • how the image is pulled and stored
  • how the sandbox is configured and booted
  • how a command gets into the guest
  • what a stopped instance means

Brig links no runtime code. It runs each runtime, cosign and oras as a subprocess. Brig has three direct Go dependencies:

Dependency Used for
sigs.k8s.io/yaml The profiles
golang.org/x/sys The terminal and process calls
github.com/godbus/dbus/v5 The Linux secret store

Runtime commands #

From internal/runtime/hull.go, unless another file is named:

hull --version                          # does this hull boot a digest?
hull assets pull                        # HULL_BOOT_ASSETS=<dir> in the environment
hull assets dir
hull ps
hull ps -a                              # falls back to `hull ps` if -a is refused
hull run --detach --name <name>
     --hypervisor <vz|hvi|qemu> --net <shared|none>   # none is --network offline
     --pull <missing|always|never> --mem <MB> --cpus <n>
     [--rootfs-type <block|virtiofs|9pfs>]
     [--annotation com.urunc.unikernel.bootKernel=<path>]
     [--annotation com.urunc.unikernel.bootInitrd=<path>]
     [--gateway-sock <path> --gateway-cidr <cidr>]
     [--shared-dir <host>:<guest>[:ro]]...
     [--gui [--gui-title <title>]]
     [--env <NAME>|<NAME>=<value>]... <image>
hull exec [-t] [--cwd <dir>] [-u <user>] [--env <NAME>|<NAME>=<value>]... <name> -- <cmd>...
hull logs [--follow] [--tail <n>] <name>
hull stop <name>
hull rm <name>
hull network-gateway --help             # does this hull enforce a policy? (--egress-default)
hull network-gateway --socket <path> --qemu-socket <path>.qemu
     --subnet 198.18.0.0/24 --gateway-ip 198.18.0.1   # internal/runtime/gateway.go
hull network-gateway --socket <path> --qemu-socket <path>.qemu
     --subnet <a /30 of its own> --gateway-ip <first address on it>
     [--egress-default <allow|deny>]                  # an isolated sandbox, or one
     [--egress-allow <rule>]... [--egress-deny <rule>]...   # carrying a policy

Every exec goes through hull exec. The reachability probe, the captured read, the credential written over stdin, and the terminal handover all build the same argv (execArgs). The handover replaces the Brig process with hull, so the guest gets a real terminal.

brig logs <ref> runs hull logs <name>. If a sandbox does not come up, Brig's error output also names that command.

Brig picks the sandbox subnet from 198.18.0.0/15, the range RFC 2544 reserves for network benchmarking. The public internet never routes that range and almost nothing claims it, so a collision with a network you need is unlikely. The other ranges are crowded:

Range Used by
10.0.0.0/8 Corporate VPNs and cloud VPCs
172.16.0.0/12 Docker
192.168.0.0/16 Home routers and vmnet on macOS
100.64.0.0/10 Tailscale
198.19.0.0/16 OrbStack. Brig leaves this sibling range alone.

From internal/runtime/nerdctl.go:

nerdctl image inspect --format {{index .RepoDigests 0}} <ref>   # the digest to pin
nerdctl ps --filter name=^<name>$ --format {{.Names}}
nerdctl ps -a --format {{.Names}}\t{{.Status}}
nerdctl network ls --format {{.Name}}
nerdctl network create <name>            # --network isolated: a network per sandbox
nerdctl network rm <name>                # with the sandbox, and by rm --all
nerdctl run --detach --name <name>
     --runtime io.containerd.urunc.v2         # BRIG_CONTAINERD_RUNTIME overrides
     --memory <MB>m --cpus <n>
     [--pull <missing|always|never>]
     [--network none | --network <name>]      # offline, or isolated
     [--annotation com.urunc.unikernel.bootKernel=<path>]
     [--annotation com.urunc.unikernel.bootInitrd=<path>]
     [--annotation com.urunc.unikernel.hypervisor=cloud-hypervisor]
     [-v <host>:<guest>[:ro]]... [--tmpfs <path>:<options>]...
     [-e <NAME>|<NAME>=<value>]... <image> sleep infinity
nerdctl exec -i [-t] [-w <dir>] [-u <user>] [-e <NAME>|<NAME>=<value>]... <name> <cmd>...
nerdctl logs [--follow] [--tail <n>] <name>
nerdctl stop <name>
nerdctl rm <name>

The container runs sleep infinity. A container exits when its command exits, and the sandbox must outlive the exec that used it.

brig logs <ref> runs nerdctl logs <name>.

Verification and fetch commands #

Brig also runs two commands that are not aimed at a runtime:

oras pull ghcr.io/nofireai/hull-assets:<os>-<arch> --output <dir>
                                                # internal/runtime/bootfetch.go
cosign verify --certificate-identity-regexp <identity>
     --certificate-oidc-issuer <issuer> <image> # internal/verify/verify.go

Command environment #

Every runtime command carries HULL_TELEMETRY_PRODUCT=brig and HULL_TELEMETRY_SUPPRESS=1 in its environment. Brig lifts the suppression only for the operations a user asked for, so one Brig command counts once. DO_NOT_TRACK and HULL_TELEMETRY_DISABLED pass through untouched and win.

Only the bare variable name of a forwarded value goes in argv, so nothing readable in ps carries a secret.

Value How it reaches the runtime
A forwarded value In the environment, with a bare --env NAME (-e NAME for nerdctl) in argv
The guest's HOME, PATH, TMPDIR and XDG_* On the command line as --env NAME=value (-e NAME=value for nerdctl), because the runtime reads those names for itself
An ordinary value with BRIG_ENV_ARGV=1 On the command line. Use this for a runtime build that cannot take a bare --env NAME.
A value Brig resolved on your behalf, with BRIG_ENV_ARGV=1 Stays off the command line

See Security model.

Runtime discovery #

Brig finds the runtime in this order (internal/runtime/runtime.go):

  1. BRIG_RUNTIME names the runtime, hull or nerdctl. Brig refuses any other value by name. If the variable is unset, the runtime is hull on macOS and nerdctl everywhere else.
  2. BRIG_RUNTIME_BIN is the executable to run. Brig takes a path as it stands, and looks up a bare name on PATH. Brig reports a missing or non-executable one against the variable before anything runs.
  3. A profile's runtimeBin does the same thing without a variable per shell. It loses to BRIG_RUNTIME_BIN. Brig expands a leading ~. Brig reports a missing or non-executable path against the profile that named it.
  4. Otherwise PATH: hull for the hull runtime, and nerdctl then docker for the other one.

brig info <ref> prints the result, as runtime hull (/opt/homebrew/bin/hull). brig doctor reports the runtime's version beside the rest of the host.

The docker fallback on Linux

If PATH has no nerdctl, Brig takes docker and says so in one line: brig on stderr, brigd in the response's warnings.

To choose docker and remove that line, name docker in BRIG_RUNTIME_BIN, or its full path in runtimeBin. runtimeBin does no PATH lookup, so a bare docker there is refused as missing.

For the limits of docker, see Runtime requirements.

Capability checks #

Brig asks the runtime four questions:

Question Command Asked of
Where do your boot assets live? hull assets dir hull
Which sandboxes exist, and in what state? ps Either runtime
Can you boot a digest reference? hull --version hull
Can your gateway enforce a policy? hull network-gateway --help hull

hull --version is the only version Brig reads. A current hull release boots a digest reference from its store, so Brig pins the image it verified. An unreadable answer counts as pinning.

Before Brig boots a sandbox that carries a policy, it reads network-gateway --help for one word, --egress-default. A sandbox with no policy runs no probe. See Policy enforcement.

With an old build, Brig degrades one feature at a time:

Runtime build What Brig does
A hull without assets dir Falls back to ~/.hull/assets
A runtime without ps -a Falls back to the plain listing
A hull that cannot pin a digest Boots the tag and says so
A gateway that cannot enforce a policy Refuses a run that carries a policy. This is the one refusal.
A hull too old for a flag Brig passes Fails at that flag, with hull's own message

Runtime requirements #

hull must accept the verbs and flags listed in Runtime commands, and three more things.

Requirement Why Brig needs it
A bare --env NAME, which takes the value from hull's own environment Without it the only way to forward a credential is BRIG_ENV_ARGV=1, which puts values where ps can read them.
exec -u root Brig uses it to mount a tmpfs inside a running sandbox (internal/wrap/secretfiles.go).
network-gateway, for the hvi backend That backend has no built-in egress, so Brig starts a gateway.

Brig starts one gateway for each isolated sandbox. Sandboxes that request shared use one shared gateway. Brig assigns the addresses on those networks.

Warning Guests on one shared gateway reach each other. See Security model for the answer per backend.

Gateways started by older versions

A gateway started by an older Brig has no API socket, so it cannot publish a port. The next boot replaces it when no sandbox is on it and no other boot is starting on it.

nerdctl must do the following:

  • carry --annotation through to the shim
  • take --runtime
  • honour -v, --tmpfs and a bare -e NAME

urunc must read com.urunc.unikernel.bootKernel and com.urunc.unikernel.bootInitrd from the container's OCI spec and boot the image with them. It is the same pair hull takes on its command line. Brig also passes com.urunc.unikernel.hypervisor=cloud-hypervisor on every genericBoot run, so urunc must find a cloud-hypervisor binary.

The runtime bundle builds its urunc from the feat/unchanged_containers-exec-fixes branch of urunc-dev/urunc, which implements the pair. It builds its container-initrd from the same commit.

containerd must be running with the urunc shim installed.

docker, your own urunc, and runc

docker is accepted in place of nerdctl and works for an image that carries its own kernel. docker does not pass annotations to the runtime, so Brig refuses a genericBoot profile on it. Without the annotations, the sandbox has no kernel.

Your own urunc. No urunc release reads the annotation pair, v0.8.0 included. A release ignores both annotations and looks for a urunc.json in the image. A stock image has none, so the sandbox never becomes ready. brig doctor still reports the runtime and the boot assets as ok. A host that brings its own urunc (BRIG_INSTALL_RUNTIME=0) also needs a build from the feat/unchanged_containers-exec-fixes branch.

runc. BRIG_CONTAINERD_RUNTIME=runc asks for a plain container, which shares the host kernel. That is the weaker boundary. The envelope's ISOLATION row says which one a run got. Security model says what the weaker one costs.

Policy enforcement #

A run path is a runtime with one backend. Before a run that carries an egress policy, Brig asks whether the run path enforces the policy. The answer comes from one table, shown in Policies.

Run path Answer Probe
hull on hvi enforced network-gateway --help must exit zero within 30 seconds and list --egress-default
hull on vz or qemu cannot enforce None
nerdctl or docker, on any shim cannot enforce None
hull on a backend the table does not name unknown

Brig boots a policy-bound run only on enforced. The refusal names the property, the runtime and the backend.

On hvi, the probe can give two other answers. Brig refuses the run on both.

Probe result Answer
The gateway does not take --egress-default. It drops the rules. cannot enforce
The probe fails: the binary does not run, it exits non-zero, or it gives no answer within 30 seconds unknown

Versions and pins #

Brig's source pins no version of hull, nerdctl, containerd or urunc, and verifies no digest of any of them. The pin is on the install path.

The hull cask in brig-sh/homebrew-brig names one release tarball and its sha256. Brig's cask depends on that cask, so brew install --cask brig gets the build the tap names. Casks/hull.rb shows what an install gives you.

Before hull writes the boot bundle, it verifies the bundle's signature with cosign against the publishing workflow. It records the digest it verified.

The pin is RUNTIME_VERSION in install.sh, which names one release of the runtime bundle. If cosign is available, install.sh verifies the signature on that release's checksums.txt before it runs the bundle's installer. The bundle's pins.env records the urunc commit it was built from, and brig-ctl version prints it.

The runtime bundle does not use the boot bundle. Its launcher points BRIG_BOOT_ASSETS at the kernel and initrd the bundle carries. Its release signs a record of their digests, and the bundle's installer keeps that record beside them.

Hosts without the runtime bundle

The boot bundle arrives through oras, which verifies no signature itself. To pin a version or point at a mirror, set BRIG_BOOT_ASSETS_REF.

After an oras fetch by digest, Brig writes a provenance.json of the shape hull writes. A later run can then tell an older bundle from a changed file.

Boot bundle digests #

On both platforms Brig compares the kernel and initrd with a list of digests before it boots them. The list comes from the verified bundle, or from the signed record in the Linux runtime bundle. See Security model.

Brig fetches the boot bundle by the digest whose signature it verified, not by the tag. hull gets repo@sha256:... as HULL_BOOT_ASSETS_REF, and oras pulls the same. A tag that moves between the verification and the fetch cannot deliver an unverified bundle.

Setting Behaviour
BRIG_BOOT_ASSETS_REF Pins a version of ghcr.io/nofireai/hull-assets the same way
HULL_BOOT_ASSETS_ALLOW_FOREIGN hull refuses a reference in any other repository unless this is set. Brig does not set it.
No verified digest and BRIG_BOOT_ASSETS_REF unset Brig drops any HULL_BOOT_ASSETS_REF from the environment it hands hull

Swaps #

These swaps need no code:

Swap How
A different build of the same runtime BRIG_RUNTIME_BIN, or runtimeBin in a profile
docker in place of nerdctl It is already in the PATH search, with the genericBoot limitation in Runtime requirements
A different containerd shim BRIG_CONTAINERD_RUNTIME
A different hypervisor backend under hull BRIG_HYPERVISOR, or hypervisor: in a profile

Any shim that reads the two boot annotations can replace urunc with no change in Brig. The envelope's ISOLATION row names the shim. If Brig cannot place a shim other than urunc's, the row calls the boundary unknown and not a microVM.

A third runtime needs a code change. BRIG_RUNTIME accepts only hull and nerdctl. Make two changes in internal/runtime/runtime.go:

  1. Implement the Runtime interface.
  2. Add a case to DetectFor.

The interface lists kind, binary, running, list, run, probe, output, feed, replace, stop, remove, and logs hint. Brig needs nothing else from a runtime.

A replacement for hull must boot an OCI image as a microVM and exec into it. Guest homes, credentials and profiles do not change.

Shared components #

Ownership Components
Shared with other projects containerd, nerdctl, urunc, cosign and oras. None of them depends on Brig.
Shared between Brig and hull The boot bundle, the on-disk layout it lands in, and the two annotation names. A machine that has run either runtime has already seeded the other.
Built for this stack hull, vz-runner and hvi. hull is a general microVM runtime and does not depend on Brig.
Brig's alone Profiles, the secret store and its provenance records, the credential forwarding rules, the billing denylist, the guest home contract, image verification policy and brigd

Hull from source #

This section applies only if you work on hull, and only on macOS.

The released hull is signed, notarized and stapled, so brew install --cask brig needs none of the steps below. A build of Brig from source needs none of them either. Brig holds no entitlement and drives the hull it finds.

A from-source hull cannot boot a microVM unless it is signed with an Apple identity. Two binaries need an entitlement:

Binary Talks to Entitlement it needs
vz-runner Virtualization.framework com.apple.security.virtualization
hvi Hypervisor.framework com.apple.security.hypervisor

macOS honours an entitlement only on a binary signed with a real Apple identity: an Apple Development or Developer ID Application certificate.

An ad-hoc signature (codesign --sign -) gets the entitlement honoured only with AMFI disabled. To disable AMFI, you must disable SIP and set a boot argument.

A copy of an entitled binary loses its signature. Re-sign the binary after every build and every copy.

If you give hull's make macos a CODESIGN_IDENTITY, it builds and signs all three binaries. hull's README documents the entitlement plists. The shipped Brig profiles ask for the hvi backend, so a from-source build needs a signed hvi binary as well as a signed vz-runner.

Type a command, a flag or an error message.