Guides
Guest image requirements
On this page
Brig boots an OCI image and then runs commands inside it. The commands are a readiness probe, the mounts that keep a credential off host disk, and the write of the credential file. An image must carry what those commands need.
A stock distribution image meets every requirement. The list matters when you build a smaller image.
Each entry names the source file it comes from. To check an image you built, see Checking an image.
Guest images for the built-in profiles are open source, built in brig-sh/community-images. That repository documents how to build an image from scratch, at bring-your-own-image.md. Profiles covers the fields that name the image and hand it a credential.
Binaries #
Brig resolves each binary through the guest's PATH, except /bin/true.
| Binary | Why Brig runs it | Source |
|---|---|---|
/bin/true |
The readiness probe. The runtime reports a sandbox as running when the VMM starts, seconds before the in-guest agent binds its listener. Brig execs this until it succeeds | internal/wrap/run.go, waitReady |
cat |
Reads the marker back out of the guest home. Reads /proc/self/mountinfo and /proc/swaps. Also the body of the exec that writes a credential: sh -c 'cat > "$1"', with the value on stdin |
internal/wrap/run.go, guestMountsWorkspace. internal/wrap/secretfiles.go, guestMountpoints, verifyVolumes, writeSecretFile |
sh |
Three short scripts: create a credential file with set -C, write the value into it, create a mount target |
internal/wrap/secretfiles.go, writeSecretFile, createGuestTarget |
stat |
stat -c '%F|%U|%a' proves a credential's target is a regular file of the right ownership and mode before the value goes into it. stat -c %s proves it is not empty afterwards. stat -f -c %T reports the filesystem a path sits on |
internal/wrap/secretfiles.go, verifySecretFile, writeSecretFile, guestFstype |
mount |
mount -t tmpfs covers a directory the profile declares. mount --bind pins each hostmount out of the way first and binds it back after |
internal/wrap/secretfiles.go, mountVolumes |
mkdir |
Creates mount targets, as mkdir -p and inside a shell script as mkdir -p -- "$(dirname "$1")" |
internal/wrap/secretfiles.go, createGuestTarget |
dirname |
Used inside that same script. It is an external command, not a shell builtin | internal/wrap/secretfiles.go, createGuestTarget |
chown |
Hands the covered directories to root across a credential write and back to the guest user afterwards. Sets the owner of the credential file itself | internal/wrap/secretfiles.go, chownGuest, writeSecretFile |
chmod |
Sets the mode a files: binding declares, inside the create script |
internal/wrap/secretfiles.go, writeSecretFile |
rm |
rm -f -- at the credential path before creating it, so a planted symlink is removed, not followed |
internal/wrap/secretfiles.go, writeSecretFile |
sleep |
Linux only. nerdctl runs the container as sleep infinity. A container exits when its command does, and the sandbox has to outlive the exec that uses it |
internal/runtime/nerdctl.go, runArgs |
bash |
brig sh runs bash -l, and brig sh <agent> '<command>' runs bash -lc, for every profile regardless of its binary: field |
internal/wrap/run.go, Shell |
The profile's binary: |
brig run execs it: claude for claude-code, codex for codex, and so on |
cmd/brig/main.go, runAgent |
When each binary is needed #
| Binaries | Needed when |
|---|---|
/bin/true, cat, the profile's binary: |
Always |
sleep |
On Linux. Treat it as required |
bash |
Someone runs brig sh |
sh, stat, mount, mkdir, dirname, chown, chmod, rm |
The profile declares volumes: or files: |
sleep. On the hull path, the image's entrypoint runs. A macOS-only image can work without sleep and then fail on Linux. The images of the published profiles are multi-arch, so treat sleep as required.
bash. Only brig sh uses it. Without bash, you cannot open a shell to look inside a sandbox.
sh to rm. A profile with neither volumes: nor files: returns before any of these run (deliverSecretFiles in internal/wrap/secretfiles.go). Two of the eight built-in profiles declare them: claude-code and claude-desktop, the two that deliver a credential as a file.
Warning If you build against the narrower list and later add one
files:binding, the image needs the whole table. The run then fails at delivery and not at boot.
stat flags #
The -c and -f format flags are the GNU coreutils spelling. busybox also implements them if it is compiled with its format feature. BSD stat spells them differently and fails the run.
Right after Brig creates a file, the answers must be:
| Format | Required answer |
|---|---|
%F |
regular file or regular empty file |
%U |
The owner's name |
%a |
The octal mode |
-f -c %T |
The filesystem type as a word. tmpfs for what Brig mounted |
Paths and kernel interfaces #
/proc, mounted. Brig reads/proc/self/mountinfoto find what is already a mountpoint. It does not runmountpoint -q, because a minimal image can lack that binary. Brig reads/proc/swapsto detect swap. If that file shows more than a header line, Brig does not hand the sandbox a credential. A tmpfs page in swap is a credential on a disk./run, writable by root. Brig pins each hostmount at/run/brig/persist/<escaped path>while the tmpfs goes over its real location, then binds it back./runkeeps the pin off the workspace, so the pin never reaches host disk and never survives a boot. The built-in profiles run the guest as root, so the agent can reach the pin. SeepersistRootininternal/wrap/secretfiles.go./bin/true, at that literal path. The probe does not resolvetruethroughPATH.- tmpfs in the guest kernel. It must accept
size=,mode=0700,nodevandnosuid. ramfs does not qualify, because it ignoressize=without a message. A guest process can then exhaust the sandbox's memory through a mount Brig created. SeeTmpfsOptionsininternal/profile/volumes.go. - No swap. The guest has no swap today. A guest that turns swap on stops the run.
Guest user #
Brig derives the guest account from the profile's guestHome:. It takes the last path element: /home/claude means the user claude (GuestUser in internal/profile/profile.go). There is no field to set the user separately.
The image must meet three requirements:
| Requirement | Why |
|---|---|
| The account exists in the image | Brig passes the name to chown inside the guest, so it has to resolve in /etc/passwd |
| The image runs as that account | Every exec except the privileged ones goes in with no -u flag, so the image's configured USER decides who the agent runs as. Set USER to the guest user, and set its home to guestHome |
| Root is available to exec as | The mounting and file-writing execs carry User: "root" (guestRootUser in internal/wrap/secretfiles.go). Only the mount syscall needs the privilege |
An image whose home directory is /home/claude but which has no claude user fails the run with could not hand /home/claude/.claude to claude in the sandbox.
Root guests #
The five shipped agent profiles set guestHome: /root, so the guest is root. A rootless Linux install maps container uid 0 to the invoking user. That lets the guest open /dev/kvm and own the workspace it writes. Each built-in profile carries the rationale next to the field. See Built-in profiles.
On a profile whose guest is root, the agent and the privileged execs are the same account. The boundary is the VM, not the guest account. Brig claims nothing about what the agent can reach inside the sandbox.
A profile whose guestHome sits under a real user's home keeps the two accounts apart. The symlink guards in writeSecretFiles cover that case.
ubuntu sets guestHome to /root/work, so the derived name is work. That is not an account in that image. Nothing reads the name there, because the image already runs as root.
Root capabilities #
The privilege to mount comes from how the image is booted.
| Boot | What root gets |
|---|---|
| Through Brig | A microVM. Root has every capability |
A bare hull run <image> or nerdctl run <image> |
An ordinary container. Root holds only the runtime's default capability set, with no CAP_SYS_ADMIN |
Brig never boots the bare way. It passes the profile's hypervisor and rootfs type. For a genericBoot profile it also passes the kernel and initrd annotations. See runArgs in internal/runtime/hull.go, and the nerdctl equivalent in internal/runtime/nerdctl.go.
Under a bare boot, mount -t tmpfs fails with permission denied, even in an image that works under Brig. For that reason, script/check-guest-image.sh boots through brig run -d.
Generic boot #
genericBoot: true in a profile means that the image has no kernel and no urunc metadata. Six of the eight built-in profiles set it. That includes ubuntu, which boots docker.io/library/ubuntu:latest unmodified. claude-desktop and cursor leave it off.
An image that carries its own kernel and urunc metadata leaves genericBoot off. This section does not apply to it.
Brig supplies the kernel and initrd, and passes them as two OCI annotations:
com.urunc.unikernel.bootKernel
com.urunc.unikernel.bootInitrdThe pair is the same on both operating systems (internal/runtime/boot.go). Brig never reads them from the image's metadata, so an image cannot nominate a file on the host.
The kernel file is named Image on arm64 and bzImage on x86_64. The initrd is container-initrd on both.
hull takes the annotations on its command line.
Brig looks for the files in the directory that hull assets dir reports.
If the files are missing, hull fetches them. It downloads the same bundle for its own use.
urunc reads the annotations out of the container's OCI spec. This takes the urunc that the runtime bundle builds, because no urunc release reads the pair. See Runtimes.
Brig looks for the files in $XDG_DATA_HOME/brig/assets. The default is ~/.local/share/brig/assets. The Linux runtime bundle sets BRIG_BOOT_ASSETS to the pair it carries.
If the files are missing, Brig fetches them with oras. See Install.
Brig refuses docker for a genericBoot profile, because docker does not carry annotations through to the runtime. Use nerdctl, or point BRIG_RUNTIME_BIN at it (internal/runtime/nerdctl.go).
If BRIG_BOOT_ASSETS is set, the files come from that directory on both platforms. Brig does not fetch into that directory.
A zero-length file counts as missing.
With genericBoot, the image does not have to carry a guest agent. The agent comes out of the initrd and is copied into the guest. Brig can then exec into a stock image.
See genericBoot in Profiles for the field itself.
Tmpfs mounts #
Brig creates one tmpfs per kind: tmpfs entry in the profile's volumes:. The mount point is guestHome plus the entry's path. The options are size=<size>,mode=0700,nodev,nosuid, with a default size of 64m.
The guest home is a host directory. A tmpfs over part of it has no path to the host, which keeps a credential off host disk.
How Brig makes the mounts depends on the runtime.
hull has no create-time tmpfs, so Brig mounts them with a privileged exec, in three phases:
- Pin every hostmount that sits under a directory about to be covered.
- Mount the tmpfs.
- Bind the pins back in through it.
Any other order loses the state that the hostmounts keep.
nerdctl gets them in the create request, as --tmpfs and -v. A container runtime has no privileged exec to mount with (createTimeVolumes in internal/wrap/secretfiles.go).
On both runtimes, Brig checks the result from inside the guest. Each covered directory must read as tmpfs, and each hostmount must not. A guest that cannot answer fails the run.
The covered directory stays root-owned until every credential is written. Brig hands it to the guest user last. There is no window in which the agent can plant a symlink at a credential's path.
Volumes and files #
volumes: and files: paths are relative to guestHome, and neither can escape it. See volumes and files in Profiles for the fields.
The mount covers the guest home entirely. A host directory is mounted over guestHome. Anything the image ships inside it is invisible once the sandbox is up. Dotfiles baked into /root at image build time never appear. Put them somewhere else, or have the agent create them on first run.
volumes: targets do not have to exist in the image. Brig creates the host-side path in the guest home before the sandbox is created. It goes through an os.Root, so a planted symlink is refused, not written through. Brig also creates the guest-side target under a directory root owns.
Brig does not change the kind of something already there. A bind mount onto the wrong kind of target fails. A directory in the guest home where the profile says file: true stops the run, and the error names both.
A tmpfs entry hides what the guest home had under that path, not what the image had. The guest home mount already hides the image's copy. What the agent writes into the tmpfs is gone at shutdown. A hostmount under the tmpfs keeps its path.
After the tmpfs is on, Brig checks that every hostmount is a mountpoint. If one is not, the run fails.
files: targets must land in a declared tmpfs. Brig refuses a profile with a files: path that no tmpfs covers. An uncovered target puts a credential in the guest home, which is host disk.
A files: binding is an ordinary file, never a bind mount. Agents rewrite a credential atomically: temp file, then rename. Rename onto a mountpoint returns EBUSY. So Brig creates, checks and fills the file through the three execs listed under sh in Binaries. The image needs a stat that answers them.
Minimal image #
A small image works if it carries the list:
FROM alpine:3.20
# The utilities above. busybox already provides most of them; coreutils and
# util-linux are here so you do not have to know which busybox features your
# base was compiled with.
RUN apk add --no-cache bash coreutils util-linux
RUN adduser -D -h /home/mine mine
COPY mine /usr/local/bin/mine
USER mine
WORKDIR /home/minePair it with this profile:
name: mine
image: docker.io/me/mine:latest
guestHome: /home/mine
binary: mine
genericBoot: true
mem: 2048
cpus: 2A profile of your own must set mem: and cpus:. Brig refuses a profile that leaves either at zero, so a file without them is skipped and not imported.
Import the profile, then check the image under it:
brig agent import mine.yaml
script/check-guest-image.sh docker.io/me/mine:latest mine| Base | Result |
|---|---|
| Debian, Ubuntu | Pass as they ship. bash, coreutils and util-linux are all in the base |
| Alpine | busybox provides every name on the list. Whether a given busybox was compiled with the stat format flags Brig parses depends on the build. The two extra packages above remove that doubt |
Run the script on your image to check either row.
Scratch and distroless #
FROM scratch with one static binary is missing the entire list. The failures, in rough order:
| What is missing | What happens |
|---|---|
sleep |
On Linux, the container exits immediately. Brig runs it as sleep infinity, so the sandbox is gone before the first probe |
/bin/true |
The readiness probe never passes. Brig waits out BRIG_READY_TIMEOUT and reports sandbox did not become ready, which says nothing about the image |
cat |
The stale-share check cannot run. Brig cannot read the marker back and treats the guest as not mounting this guest home |
sh, stat, mount, mkdir, dirname, chown, chmod, rm |
No credential is delivered. Nothing between the tmpfs and the credential file happens |
/etc/passwd |
chown has no name to resolve. The guest user does not exist even if the binaries did |
bash |
brig sh cannot get you in to look |
Distroless images are in the same position. The static and base variants carry no shell and no coreutils, so every row above applies except the last two. They ship an /etc/passwd with a nonroot user, and the missing bash is the smallest of the problems. The :debug variants add a busybox shell, which is still not the whole list.
Tip If you want a static binary in a tiny image, put it in a distribution base, not in
scratch. The cost is a few megabytes of userland.
Checking an image #
script/check-guest-image.sh <image> [profile]The script is script/check-guest-image.sh in the brig repository. It runs the requirements against a real image and prints one line per requirement.
The profile defaults to claude-code. A profile of your own must be one that Brig knows, through brig agent import.
The script boots the image with BRIG_IMAGE=<image> brig run -d <profile>@image-check, in a scratch guest home. It removes the sandbox with brig rm at the end. A boot through Brig supplies the hypervisor, the rootfs type, the generic-boot annotations and the capabilities described in Root capabilities.
The profile decides what the script checks:
| Profile value | What the check uses it for |
|---|---|
guestHome: |
The guest home |
Last path element of guestHome: |
The user that chown has to resolve |
binary: |
The agent CLI the last line looks for |
The requirements run as root through the runtime's exec against the sandbox Brig created, one exec per line of the Binaries table. They report behaviour, not the presence of a file.
If the image does not come up, the script falls back to listing the image filesystem, and says so. Those checks are presence only.
The script sets BRIG_VERIFY=off for the boot, so Brig does not refuse an unsigned image.
Warning The check is a real run of a real profile. The credentials that profile delivers are delivered into the image under test.
The script is not part of CI, which has no registry access and no runtime to boot with. Run it against an image you build.
| Exit status | Meaning |
|---|---|
0 |
Every requirement is met |
1 |
Something is missing |
2 |
There was nothing to check with: no brig, no runtime, or no such profile |
The script never reports a pass it did not perform.
To print what the script plans to boot and stop before the boot, set BRIG_DRY_RUN=1. This checks the argument handling and the profile lookup on a machine with no runtime.