brig docs

Security

Security

On this page

You decide what an agent in a Brig sandbox can reach: its files, its credentials and its network. Each section states one boundary and how far it goes. Claims lists the tests that defend these promises.

To report a flaw in a boundary, see Security policy. Known limitations lists the behaviour that is not a vulnerability.

What the agent can reach #

The guest can access:

  • its guest home, read-write
  • the project you name on the run line, read-write at /work/<name>
  • each hostmount volume a profile declares, as an additional share
  • the credentials you deliver to it, and no others
  • the internet, on either isolated or shared (see Network egress)
  • a port another sandbox listens on, when both use shared, on hvi and on Linux (see Sandbox to sandbox traffic)

Whether the guest can reach a service bound on the host is in Host services.

The host can reach the guest only on a port you publish. See Published ports.

What you give the agent is real:

  • The agent edits the real files in the guest home and the project, so its work lands in your project.
  • The agent can send what it reads there over the network. To limit where, attach an egress policy.
  • The agent can use a credential for as long as it holds it. It can misuse the credential the same way a person can.

What the agent cannot reach #

The guest cannot access:

  • any other host directory
  • your keychain
  • your SSH agent
  • your secret manager
  • an environment variable that a profile's deny list refuses

A run reads no host credential source. See Credentials.

Two limits apply to this list:

  • A deny list covers only environment variables. It does not cover a files: binding. See File delivery.
  • Every hostmount in a shipped profile is inside the guest home, so no shipped hostmount exposes anything today. A profile you write has no such guarantee.

Default network posture #

Sandbox Posture
New sandbox on hvi or Linux isolated
New sandbox on vz or qemu, with no posture named shared
The graphical claude-desktop profile shared, an explicit exception
Existing sandbox keeps its recorded posture

A flag, a setting or a profile can name another posture. See Policies for overrides and the backend limits.

isolated separates sandbox networks. To restrict internet access, attach an egress policy (see Network egress). It does not establish whether host services are reachable (see Host services). Sandbox to sandbox traffic states what isolated and shared guarantee.

The vz and qemu backends, older sessions, and custom profiles
  • The unpublished cursor profile sets no posture. It isolates on Linux and hvi, and takes the backend fallback on vz or qemu.
  • A custom profile with no network choice falls back to shared on vz or qemu. brig info names that fallback.
  • An older session with no recorded posture keeps the posture that Brig reads from the runtime.
  • If Brig cannot establish the posture of an existing sandbox, it refuses a flagless run.

Isolation boundary #

What Brig does

The sandbox is a microVM on macOS and on Linux, so the guest has its own kernel.

hull boots the microVM. brew install --cask brig installs hull.

Six of the eight built-in profiles use hull's hvi backend. hvi is a microVM monitor that drives Hypervisor.framework directly. The other two profiles use the vz backend, on Virtualization.framework. Runtimes compares the backends.

By default, Brig drives nerdctl and hands the container to the urunc shim (io.containerd.urunc.v2).

BRIG_CONTAINERD_RUNTIME=runc selects a plain container, which shares the host kernel. That boundary is weaker. You get it only when you set the variable.

The ISOLATION row of the execution envelope names the boundary you got. brig info prints the row and boots nothing. brig --verbose run prints it before the boot.

ISOLATION    microVM (hull, hvi backend)
ISOLATION    microVM (hull, vz backend)
ISOLATION    microVM (nerdctl over containerd, io.containerd.urunc.v2)
ISOLATION    container (docker over containerd, runc: the guest shares the host kernel)

The guest has only the host directories listed in What the agent can reach. The guest cannot fetch credentials from the host, so you must deliver them explicitly. See Credentials.

Limits

  • The ISOLATION row reports what this run resolved: the binary, the backend and the shim name. That report is not the same as the promise above.
  • Brig can boot a sandbox with a shim it does not recognise. Brig cannot establish the isolation of that sandbox from the shim name. The row then says that it cannot tell.
  • A hostmount that your own profile declares outside a tmpfs cover is a host path the guest can see.

Published ports #

What Brig does

Nothing on the host can open a connection into the sandbox until you publish a port. To publish a port, use --publish on a run, or brig network publish on a running sandbox. brig network unpublish removes the port.

A published port binds to 127.0.0.1 unless you write an address. The port is then reachable from this machine, and not from the network this machine is on. To bind wider, write the address, as in 0.0.0.0:8080:80.

The PORTS row of the execution envelope names each published port:

NETWORK      isolated (a network of this sandbox's own)
PORTS        127.0.0.1:8080 -> 80
             0.0.0.0:443 -> 443 (reachable from the network this host is on)

The row lists every port the sandbox publishes, not only the ports this command line asked for. A publication outlives the run that made it. The row marks an address other than loopback.

Limits

A published port is ingress, so no egress rule applies to it. A policy controls the connections the guest can open, and not the connections opened into the guest. A sandbox under a strict egress policy is still reachable on a published port.

Credentials #

What Brig does

A run reads no host credential source. The brig run, exec and shell paths do not reach:

  • a keychain item Brig did not write
  • a credential file outside the guest home
  • a host command that produces a credential

Your host login enters the Brig secret store once, when you run brig secret import <profile>. Every later run reads only that store:

brig run claude-code               # log in inside the sandbox, or:
brig secret import claude-code     # carry the host login in, once

The profile selects one of two channels for each secret.

Channel What it does
files: writes the credential into the guest at the path the agent already reads
env: binds it as an environment variable, for credentials whose consumer offers no file interface

For an env.<name> binding, or the deprecated forward: spelling of one, Brig reads the named variable from its own environment. Any tool that populates that environment still works as a backend for those bindings.

Brig reads the values again on every exec, so the guest gets a rotated credential with no sandbox restart. Brig writes nothing into the guest home from the host for this.

Limits

Brig makes two host reads on every run. No setting turns them off.

Read Purpose
git config --get in the directory you invoked Brig from, for user.name, user.email and github.user resolves the commit identity forwarded into the guest
The user: line from the stanza for your git host in gh's hosts.yml, under $GH_CONFIG_DIR or ~/.config/gh names the login that pairs with the forwarded token

Brig reads hosts.yml only when github.user is empty. That file usually also holds the OAuth token of gh. Brig takes only the login. BRIG_GIT_IDENTITY=0, BRIG_GIT_CONFIG=0 and BRIG_GIT_USER change how Brig uses the answers. They do not stop the reads.

On Linux, a host with no keyring has no secret store. A run then does this:

Secret Behaviour on a host with no keyring
Optional, as in claude-code the run boots and the agent asks for a login
Required the run fails, because that host has no place to read the secret from

File delivery #

What Brig does

A credential delivered as a file:

  • stays out of /proc/<pid>/environ
  • is not inherited by processes the agent spawns
  • can be rewritten under a running agent, so a rotated secret can reach a live session. An environment variable cannot do that.

The file lands on a memory-backed mount and does not reach your disk. See Host disk.

Limits

Brig stores and hands over a refresh token. Claude Code needs refreshToken and refreshTokenExpiresAt in .credentials.json to work. With only an access token in the file, the agent attempts a refresh, fails, and prompts. A compromised agent inside the sandbox can therefore mint access tokens indefinitely, also after the host's token expires. In return, the guest refreshes its own token, so a long session does not break every few hours.

Brig's copy is less protected than the item it came from. The host's Claude item has an ACL scoped to the application that wrote it. A first read by anything else therefore raises a dialog. Brig's copy carries the default ACL, as does every secret in the Secret store. The only mitigation is to keep the stored copy low-value, and a refresh token is not low-value. When you do not use the copy, delete it: brig secret delete claude-credentials.

The denylist stays env-scoped. A files: binding bypasses the deny check, and no name check can fix that. A profile can deliver a metered API key inside a settings.json, and nothing detects it. deny catches accidents: an ambient variable from your shell that reaches the guest. A file binding takes an explicit stored secret and an explicit binding from the profile author. The agent takes the two names on the claude-code denylist as environment variables by design, so the list still covers that channel.

The stored copy does not rotate. A credential renewed on the host does not update Brig's copy. A credential revoked on the host stays valid in Brig's store until you import it again or delete it. Brig warns before boot when the stored copy is expired, and names the command that refreshes it. Brig cannot see a revocation.

A 0600 file is still readable by anything running as the agent's uid inside the sandbox. A file narrows the exposure, but it is not a boundary. See Exposure inside the sandbox.

Secret resolution rules #

Brig applies these rules when it resolves a secret:

  • Brig skips an unset or empty value, so that value cannot shadow a value baked into the image.
  • Brig refuses a scheme:// value read from the environment as an unresolved secret-manager reference.
  • Brig refuses a variable on the profile's deny list, and gives the reason.

direnv and similar tools leave unresolved references in the environment. A reference forwarded verbatim yields "Invalid username or token" in the guest, which looks like a broken sandbox.

Source of a scheme:// value Result
The environment refused
The environment, with BRIG_ALLOW_REFS=1 forwarded anyway
A value: literal skips the check
Brig's own secret store skips the check

brig info <agent> reports the guest's environment by name and never prints a value. If a declared secret cannot be resolved, it fails the same way a run does. It annotates a secret-sourced variable, for example GH_TOKEN(secret). A credential delivered as a file is not an environment variable and does not appear in that list.

Host disk #

What Brig does

The credential file does not reach host disk. It lands on a tmpfs mount that covers ~/.claude, the agent's whole config directory. Before Brig writes anything, it verifies that the mount is tmpfs with no swap. ~/.claude/.credentials.json and the temp file that the agent renames onto it stay off your disk.

brig stop removes that mount with the sandbox. A login made inside the sandbox on this profile therefore does not outlive a stop.

Limits

Seven paths under ~/.claude are hostmounted and are not on that mount. They live in the guest home on host disk and persist across boots:

Path Contents
settings.json, CLAUDE.md your permission allowlist and your user-level memory, written by hand or by the agent on your instruction
sessions, projects, history.jsonl the conversation
plugins, skills also where --skills copies your own. Leaving either off makes that flag do nothing.

Anything else under ~/.claude is ephemeral, including anything a future Claude Code version starts to write there. This list follows the volumes: block of the claude-code profile.

Process arguments #

What Brig does

Brig puts forwarded values into the environment of the runtime process. Only the variable name appears on the command line. Other processes on the host cannot read a forwarded credential in ps.

Limits

HOME, PATH, TMPDIR and any XDG_ variable are the exception. The runtime reads these for itself:

  • hull keeps its store under HOME and finds hvi on PATH.
  • A rootless nerdctl reads its registry config under HOME.

A guest value in the runtime's environment redirects the runtime. Brig therefore passes these on the command line as NAME=value, on every run. None of them carries a credential, and the BRIG_ENV_ARGV warning does not list them. Brig refuses a stored secret bound to one of these names.

BRIG_ENV_ARGV=1 puts forwarded values back on the command line, for a runtime build that does not accept a bare --env KEY. The guarantee then does not hold for a value read from the environment.

A value bound from Brig's secret store is exempt, and stays off the command line. The host durably logs the argv of every exec.

Exposure inside the sandbox #

Anything that runs alongside the agent inside the sandbox can read the credential. The sandbox cannot use a credential it cannot see.

Brig relies on a narrow blast radius, and does not substitute a sentinel value. Prefer a fine-grained GH_TOKEN scoped to the repositories you want reachable, over a classic PAT that carries your whole account.

Secret store #

brig secret is the only store a run reads. A profile names what it wants from the store under secrets:. brig secret import puts a host login into the store.

Prefer the store to a value composed into Brig's environment. Both reach the same variable in the same guest. Only the stored value stays off the command line under BRIG_ENV_ARGV=1. See Process arguments.

For usage, see Secrets. For the profile side, see Profiles.

The backend is the login keychain. Every item is a generic password under the service sh.brig.secret, with the secret's name as the account.

printf %s "$TOKEN" | brig secret create gh-token
brig secret create deploy-key -f ~/.ssh/id_ed25519
brig secret ls

The store is a Secret Service keyring on your session bus: gnome-keyring or KWallet. A host with no keyring has no store. brig secret says which half is missing. Brig does not fall back to a file.

The rest of this section describes the macOS keychain backend.

What Brig does

The value never appears in argv. A secret is two keychain items, written by two security invocations.

Item How it is written What the command line shows
The key the whole add-generic-password command, base64 key included, goes to security -i down a pipe security -i and nothing else
The sealed value through security's arguments the ciphertext and the secret's name

The forwarding path makes the same guarantee in Process arguments.

The key and the sealed value are separate items. See Secrets for the layout. A copy of the keychain file without the login password holds two encrypted items that it cannot open. Keychain Access shows a base64 key and base64 ciphertext, not the secret.

brig secret ls never decrypts. It reads attributes only, so it raises no access prompt. It can show names and dates but never values.

Brig writes only under its own service. Every command that creates, changes or removes an item carries -s sh.brig.secret, so Brig cannot reach outside that namespace.

A run never reads another application's item. Only brig secret import reads an item of another application, such as Claude Code's, and it never writes that item. The read raises a keychain dialog, because security did not create the item. The dialog appears once, when you ask for the import, and never on the boot path.

Limits

The item's ACL is the default one. security created these items, so security can read them back with no keychain dialog. Anything that can run /usr/bin/security as you can also read them. That boundary is the same as your shell, and it is weaker than a per-application ACL. Brig does not ask for the broad -A, and it does not narrow the default.

The two-item layout changes nothing for the threat model. Any process running as you can read the key and open the sealed item.

brig secret ls reads more than Brig's items. security dump-keychain takes no service filter. Its only options are [-adhir] [keychain...]. Brig names no keychain, so the dump covers the whole keychain search list. On a stock Mac that list is your login keychain and the System keychain. security list-keychains shows yours.

ls enumerates the attributes of every item in those keychains and discards the items that are not Brig's. Brig decrypts nothing, and nothing leaves the process. Names and dates that belong to other applications and to the system pass through Brig before it discards them.

The service name is a label, not an authenticity check. Another process running as you can add an item under sh.brig.secret. Brig then reads, updates and deletes that item as its own. read reports a value that is not in Brig's encoding, and ls skips a name outside Brig's grammar.

Reproducing the argv check

security -i reads one command per line and blocks for the next. The write is on the process table only while the pipe stays open.

  1. Run security -i against a fifo, and hold the fifo open with an idle writer.
  2. Send the real add-generic-password line down the fifo.
  3. Read ps -Ao args while the command waits. The argv shows security -i and nothing more.
  4. Run security find-generic-password to verify that the value is stored.
  5. Close the fifo.
  6. Delete the probe item.

Guest home writes #

The guest home is mounted read-write, so the sandbox controls its contents. Brig also writes into it from the host on every invocation:

  • the stale-share marker
  • the onboarding seed
  • the trust key
  • the guest git files
  • the skills copied in by --skills

Through these writes, the sandbox can influence what happens on the host. Brig runs as you, outside the sandbox. A guest can plant a symlink where Brig writes next. That symlink aims Brig at a host path the guest cannot reach, such as your ~/.ssh or your shell profile. The microVM boundary does not stop this, because the write is on the host side of it.

What Brig does

Every host-side read and write that Brig makes inside the guest home goes through an os.Root opened on it.

Path inside the guest home Read Write
Absolute symlink refused refused
Relative symlink that climbs past the root refused refused
Symlink that stays inside the guest home followed refused
Fifo, socket, device or directory where a regular file belongs refused refused

Brig decides from the file it opened, and not from an earlier check. The guest has no window between the check and the open to swap the file.

Brig writes only regular files where a state file belongs. A read follows an internal link, so that a .gitconfig symlinked to your dotfiles inside the guest home keeps working.

The path to the guest home. An os.Root cannot see a symlink at the guest home or on the path to it. Brig verifies that path separately, before it creates anything:

  1. The guest writes as you, so it can swap an entry only inside a directory you can write. Brig splits the path to the guest home at the first such directory.
  2. Above the split, the guest cannot reach any entry. Brig opens that part by name, and follows the links that the system or an administrator put there. /tmp and /var on macOS are such links.
  3. From the split down, Brig descends one component at a time against the directory it already holds. It refuses every symlink. After each step it verifies that the entry it opened is the entry it looked at.

The guest home is always in the descended part, so Brig refuses a link there wherever it sits. Brig refuses --home pointed at a symlink for the same reason, with the same kind of message. To fix it, name the real directory.

To decide whether you can write a directory, Brig asks the kernel through access(2) and adds ownership: you can always chmod a directory you own. A group membership past the first sixteen and an ACL both count, on both platforms.

Brig also refuses a link above the split whose target passes through a directory you can write. A root-owned /data that points into your home is one example. Name the real directory instead.

Deleting a guest home. brig rm deletes a guest home that Brig created.

  • The delete takes only a direct child of ~/.brig/homes.
  • It goes through an os.Root opened there, so it cannot resolve outside that directory.
  • It removes a symlink as a link and never descends into it. The target of a symlink that the guest left in its home stays untouched. os.Root alone does not give that, because it follows a link whose target stays inside the root.
  • A first run whose boot fails deletes the home it created the same way.
  • Brig never deletes a guest home you named with --home or BRIG_WORKSPACE, wherever it is.

The refusal. A refusal is a failed run, before Brig writes anything. The message names the link and its target:

$ brig run claude
brig: refusing to write /Users/alex/brig/claude-code/.claude.json: it is a
symlink to "/Users/alex/.ssh/authorized_keys", and brig writes only regular
files inside the workspace. The workspace is mounted read-write as the
sandbox's home, so that link was put there from inside the sandbox, to have
brig -- which runs as you, on the host -- reach a file the sandbox cannot.
Nothing was written; inspect /Users/alex/brig/claude-code/.claude.json and
remove it before running brig again: a symlink leads out of a directory brig
is checking

Brig does not create the links it writes through. A link in the way is one you put there, or it is the sandbox reaching for the host. Do not retry. Remove the link, or point the guest home somewhere else.

Limits

The runtime resolves the share path later, so the guest home has the same handover gap as the project. See Project mounts.

Running Brig as root

When Brig runs as root, ownership tells it nothing, because the guest also writes as root. A directory that a root sandbox had read-write looks like one of the machine's own.

Brig then trusts only the entries of /, which it never mounts. The system's links there still resolve, such as /tmp on macOS or /home on an ostree system. Brig walks every component below them link by link. It therefore refuses a link on the path to the guest home or the project.

Project mounts #

If you name a project on the run line, Brig mounts it read-write at /work/<name>. The sandbox can replace every component at or below it.

A share is a path, and the runtime resolves it on the host side of the boundary. If the project holds a planted link, the VMM exports the directory the link points at. The microVM does not stop this.

The sandbox of the run gains nothing from a swap, because it already has the project read-write. A second sandbox with write access to the path does gain. That is the sandbox of an earlier run, or one from another session that still runs with an overlapping project. The attack looks like this:

  1. An agent replaces a subdirectory with a link to somewhere else on the host.
  2. You later narrow a run to that subdirectory, which is the ordinary way to point an agent at one part of a repository.
  3. The path is yours, and the agent chose the directory it reaches.

What Brig does

Brig reaches the project by the same walk as the path to the guest home, described in Guest home writes. It splits the path at the first directory you can write, then descends and refuses every symlink. The boot descends again and refuses a path that no longer names the directory Brig holds.

A hostmount volume source gets the same walk. Its path is inside the guest home, so the guest owns every component. Brig verifies that the source is symlink-safe when the run begins. A restart reopens the boot after the guest held the workspace. Brig therefore verifies the source again through the held handle before it builds the share. Brig refuses a source swapped for a link, and does not export it.

Brig also refuses a link you made. It cannot tell that link from a planted one: same owner, same directory, same bytes. The message names the target so that you can type the target instead. The guest home refusal comes from the same walk, with different wording.

A refusal is a failed run, before Brig mounts anything. The message names the link and its target:

$ brig run claude ~/lab/monorepo/frontend
brig: refusing to use /Users/alex/lab/monorepo/frontend as this run's
project: it is a symlink to "/Users/alex/escape-target", and the project is
mounted read-write into the sandbox, so brig will not hand a guest a
directory reached through a link. Name the real directory instead: a symlink
leads out of a directory brig is checking

For a link further up the path, the message names the component that is a link, not the directory you typed:

$ brig run claude ~/lab/monorepo/frontend/src
brig: refusing to use /Users/alex/lab/monorepo/frontend/src as this run's
project: /Users/alex/lab/monorepo/frontend on the way to it is a symlink to
"/Users/alex/escape-target", so the sandbox would be handed a directory other
than the one you named. Name the real directory instead: a symlink leads out
of a directory brig is checking

The PROJECT row in the run envelope reports the directory the sandbox gets, not the path as typed. On macOS, a project under /tmp therefore prints as /private/tmp.

Limits

The checks do not close the handover. The share is a string that the VMM resolves later. In that window, a sandbox from another session that still runs with an overlapping project can swap a component. An example is ~/monorepo while this run names ~/monorepo/frontend.

The guest home and a hostmount volume source have the same gap. To close it, the runtime must accept a directory handle instead of a path.

Guest images #

An image is code that runs with your credentials, so Brig verifies its origin before the boot.

What Brig does

Brig uses cosign's keyless verification. The check asks whether one workflow in one repository built the image, and not only whether the image has a signature:

cosign verify \
  --certificate-identity-regexp \
    '^https://github\.com/brig-sh/community-images/\.github/workflows/build-images\.yml@refs/heads/main$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/brig-sh/claude-code-stock:root

That command works anonymously. The images are public, and the signature is in Sigstore's transparency log, not behind a registry login.

:root is a multi-arch index. The per-architecture tags (:arm64, :amd64) and the immutable :<arch>-<sha> pins are signed the same way and verify with the same command.

The identity is anchored on the repository and the workflow file. A signature from anywhere else fails, including one from another workflow in the same repository.

BRIG_VERIFY has three modes:

Mode Behaviour
warn the default. Reports what it found, then boots anyway, with one exception in the table below.
require refuses anything it cannot positively verify, third-party images included
off skips the check entirely

A typo in BRIG_VERIFY refuses the run and names the three values.

What warn does with the answer:

Situation Behaviour
Image under ghcr.io/brig-sh/, signature verifies one line saying so, boots
Image published by somebody else warning, boots
cosign not installed warning, boots
Image under ghcr.io/brig-sh/, signature fails stops and asks [y/N]. Refuses when there is no terminal.

Bring-your-own images are a supported way to use Brig, so warn boots an image that it is unable to verify.

If you publish signed images, point BRIG_VERIFY_REGISTRY, BRIG_VERIFY_IDENTITY and BRIG_VERIFY_ISSUER at your registry and workflow.

Limits

  • Under the default warn mode, an image published by somebody else boots on a warning. So does any image on a host without cosign.
  • claude-desktop points at a ghcr.io/nofireai/ image, and ubuntu at docker.io/library/ubuntu. Brig has no signing policy for either registry, so both warn on every boot.

Digest pinning #

A tag can resolve to different bytes in the registry and in your local store. The provenance claim is about the bytes that run.

What Brig does

For an image under ghcr.io/brig-sh/, Brig first resolves the reference to the digest the registry serves. Brig verifies that digest and boots it, so the object cosign verified is the object that runs. The success line names the digest, not the tag.

Situation warn require
The local store holds a different digest under the tag stops and asks, as for a failed signature. A yes boots the verified digest, not the copy on disk. refuses outright
The registry cannot be reached stops in the same way refuses outright

Every cosign call is bounded, so an outage fails in seconds and does not hang the boot.

The pin holds wherever the runtime's store answers a digest: containerd on Linux, and a current hull on macOS. Brig asks the hull it drives, because the hull on PATH can be older than Brig.

Limits

An image Brig did not publish carries no signature of ours to verify. Brig makes no cosign call for it and resolves no digest. It boots by tag, with one line that says whose image it is.

On macOS, hull does not yet expose the digest its store holds for a reference. The report that the local copy differs from what the registry serves is therefore Linux-only for now. The boot is pinned on both platforms.

Older hull versions on macOS

An older hull cannot find a digest reference in its store. A pinned boot there pulls again on every run, and fails under BRIG_PULL=never with the bytes on disk.

On such a hull, Brig therefore verifies and boots the tag, and prints one line that says so. The gap remains until you upgrade hull. Under the default missing pull policy, cosign verifies the tag in the registry, not necessarily the copy hull already holds. BRIG_PULL=always is the workaround. Brig does not name a digest on a boot that did not pin one.

After you upgrade hull, one more case applies. An image pulled under an older hull has no index digest on record, and a multi-arch tag resolves to its index digest. The first pinned boot of such an image misses the cache and pulls once. Under BRIG_PULL=never it fails until you pull the image again.

Kernel and initrd #

Six of the eight shipped profiles boot a kernel and an initrd that Brig downloads. The other two boot the kernel in their image.

Profiles Kernel source
claude-code, codex, gemini, grok, opencode, ubuntu the boot bundle Brig downloads, checked as described here
cursor, claude-desktop their own image. Nothing in this section applies.

cursor has no published image today. brig agent ls marks it (no published image), so a run of it fails before any of this applies.

What Brig does

Brig verifies the bundle under the same BRIG_VERIFY setting as the image (see Guest images). This is a second check, with its own trust root:

Part Value
Registry prefix ghcr.io/nofireai/
Signing identity the build-assets.yml workflow in NOFireAI/hull-assets
Issuer the same as the image check

A signature that fails stops the boot, in every mode except off. There is no [y/N] prompt.

The signature covers the bundle's manifest, and the manifest lists a sha256 for each file. Brig then does the following:

  1. It keeps the digest whose signature verified.
  2. It reads the manifest from the registry by that digest, and verifies that the manifest's bytes hash to it.
  3. It hashes the kernel and initrd it hands the runtime and compares them with that list before the boot.

brig: image and boot assets verified appears only after both files match.

Brig refuses a registry that answers with any of these: bytes that are not that digest, an index, a redirect loop, or a token realm or redirect over plain http.

The result of a difference depends on who chose the asset directory.

Case warn require
BRIG_BOOT_ASSETS unset, files match a provenance.json for another bundle Brig fetches the verified digest over them and compares again the same
BRIG_BOOT_ASSETS unset, any other file differs refuses the run. Nothing is fetched over the file. refuses the run. Nothing is fetched over the file.
BRIG_BOOT_ASSETS set, a file differs states the difference and boots. Nothing vouches for that kernel. refuses
Digests Brig cannot verify states it and boots refuses

Notes on those rows:

  • With BRIG_BOOT_ASSETS unset, Brig chose the directory and fetches into it, by the digest whose signature verified. Files that match a provenance.json for another bundle are that bundle, fetched before the tag moved.
  • The refusal names the file, its digest and the digest the bundle lists. If you delete the two files, Brig fetches the bundle again.
  • hull's store counts as a directory Brig chose wherever HULL_BOOT_ASSETS puts it, because hull fetches into it.
  • With BRIG_BOOT_ASSETS set, the directory is someone's build.
  • hull also verifies a directory against its own provenance.json. A named copy of hull's directory with a changed file fails at hull even under warn.
  • "Digests Brig cannot verify" covers four cases:
    • no manifest and no record of the verified bundle
    • a registry answer Brig refused
    • a record with no entry for one of the files
    • files that match only hull's record

BRIG_VERIFY=off skips the signature check and the digest checks, and one line says so.

A BRIG_BOOT_ASSETS_REF under ghcr.io/nofireai/ gets the same check against its own digest. Any other reference has no signature of ours, so there is no digest to bind.

BRIG_VERIFY_REGISTRY, BRIG_VERIFY_IDENTITY and BRIG_VERIFY_ISSUER repoint the image's trust policy only. The kernel's identity is fixed.

When Brig cannot read the manifest, it reads hull's provenance.json in the asset directory instead. It accepts only a record that names the verified digest.

hull writes that record into the directory it describes. Anything that can rewrite the kernel there can rewrite the record to match. For that reason:

  • Files that differ from the record still count as a difference.
  • Files that match the record are "cannot verify", never verified.
  • A registry answer Brig refused gets no fallback.

The Linux runtime bundle has its own kernel and initrd, and the boot bundle's manifest lists neither. The bundle's release lists them:

  • The release publishes share/guest/SHA256SUMS as an asset.
  • Its signed checksums.txt covers that asset.
  • The bundle's installer keeps checksums.txt, checksums.txt.sig and checksums.txt.pem beside the files.

When the directory BRIG_BOOT_ASSETS names holds a SHA256SUMS, Brig verifies that record instead of the boot bundle's signature, in this order:

  1. The kernel and initrd must hash to what SHA256SUMS lists.
  2. checksums.txt must list the SHA256SUMS in the directory, byte for byte.
  3. cosign verifies the signature on checksums.txt against the release workflow of NOFireAI/brig-standalone-linux, on a tag, with the same issuer as the image check.

BRIG_VERIFY_RUNTIME_IDENTITY and BRIG_VERIFY_RUNTIME_ISSUER point the third check at a fork's release workflow instead.

Result warn require
A file the record does not list refuses refuses
A record the release does not list refuses refuses
A signature that does not verify refuses refuses
Files match a record Brig cannot verify says so and boots refuses

A record Brig cannot verify means no cosign, no signature files beside it, or no answer from Sigstore. To put the bundle's files back, reinstall the bundle with install.sh.

The first two checks need no network, so a changed file refuses even where the signature cannot be verified. cosign verifies the signature online, as it does for the image. That takes about as long as the boot bundle's check it replaces.

Only a run through nerdctl reads the record. On hull, a SHA256SUMS in a named directory is ignored.

A runtime bundle installed without a signed record falls under the "BRIG_BOOT_ASSETS set" row above. Under require its directory refuses until install.sh installs a newer bundle.

Limits

The comparison happens before the runtime starts. The runtime opens the files later by path, after the workspace and the image pull. A first pull can take minutes. Anything that can write to the asset directory in that time can still swap a file.

The host's own user can write there. A sandbox can write there only when it mounts that directory. A user install keeps its asset directory under $HOME, and brig run claude ~ shares $HOME read-write.

Platform After Brig's check
macOS hull narrows the window. It stages a copy of each file and checks the copy against its provenance.json, so a swap has to rewrite the record too.
Linux nothing checks the files again

Release binaries #

What Brig does

Releases are signed with keyless cosign. The certificate is short-lived, bound to the release workflow's OIDC identity, and recorded in a public transparency log.

cosign verify-blob \
  --certificate checksums.txt.pem \
  --signature checksums.txt.sig \
  --certificate-identity-regexp \
    '^https://github\.com/brig-sh/brig/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

shasum -a 256 -c checksums.txt --ignore-missing

The first command vouches for the checksum file. The second ties every archive to it. Each archive also ships an SPDX SBOM.

The macOS binaries are also signed with a Developer ID certificate and notarized with Apple. cosign proves provenance, and Gatekeeper asks a different question. Neither replaces the other.

Nothing strips the quarantine attribute. It is still on the files Homebrew installed, and Gatekeeper accepts them because they are notarized.

spctl -a -vv -t install "$(which brig)"  # source=Notarized Developer ID
xattr -l "$(which brig)"                 # com.apple.quarantine, still there

Limits

A from-source build is neither signed nor notarized. You trust it because you built it.

Telemetry #

What Brig does

The runtime Brig drives sends usage events to NOFire AI, on macOS only. Nothing is sent on Linux. brig telemetry off or DO_NOT_TRACK=1 turns it off everywhere it runs.

See Telemetry for how to read the current state, and for the field-by-field list of what an event carries. That list excludes:

  • host paths, repository names, command arguments and agent prompts
  • secret names and values, image references, network destinations and file metadata

hull gives you these ways to inspect what it sends:

  • HULL_TELEMETRY_DEBUG=1 prints payloads to stderr and does not send them, so you can read what an event carries.
  • The install identifier is a random value hull stores in ~/.hull/telemetry.json. If you delete the file, the identifier rotates.
  • Crash reports queue in ~/.hull/crashes/. You can read or delete a report there before hull uploads it.

The commitment also drops IP addresses at ingestion and keeps raw events for a year.

Limits

The field list is hull's stated commitment. The Brig repository does not verify it.

If you find one of the excluded items in a payload, that is a bug. Report it as described in Security policy.

Known limitations #

Brig does not claim any of the following. A report that Brig behaves this way describes a known limitation, not a vulnerability.

Network egress #

You control outbound traffic with a posture and an egress policy.

  • --network offline gives the sandbox no route out, on every backend.
  • On hvi, an attached egress policy limits the sandbox to the hosts and ranges you allow.
  • With no policy attached, a sandbox has open internet access, on every backend. That is what brig run <agent> gets on a fresh install.
Backend Egress policy
hvi enforced at the network gateway Brig gives that sandbox
vz, qemu, Linux no policy is available. Outbound traffic is whatever the runtime allows.

Every runtime Brig ships refuses to boot a policy it cannot enforce. It does not boot the sandbox unconstrained. Each adapter makes that refusal. Brig does not impose it on every runtime it will ever drive. See Policies.

The one exception is --network offline. A sandbox with no route out satisfies every rule set, so no backend refuses a policy on it.

Host services #

Brig does not say whether the guest can reach services bound on the host. That covers a dev server, a local model, an MCP server, and a metadata endpoint.

Brig adds nothing to narrow that. The guest's network reaches what the runtime's default allows. Whether that includes the host is unmeasured on every backend.

The one control is an egress policy on hvi, with default: deny and no cidr allow for the host's own ranges. vz, qemu and Linux have no equivalent.

Sandbox to sandbox traffic #

Brig does not promise that one sandbox cannot reach another under a shared network. Default network posture states which sandboxes are on shared.

Brig asks the runtime for its shared network. On hvi, Brig also hands out the addresses on it. The runtime, not Brig, decides whether that network forwards traffic from one guest to another.

The measurements are in the sandbox reachability record.

Backend Can one sandbox reach another on shared?
hvi on macOS Yes. Measured on 2026-09-27 with brig v0.3.0: one sandbox fetched a file over HTTP that only the other served. With --network isolated on both, the same request timed out. A measurement on an older hull gave "no" on the shared network. What changed the answer is not known.
vz on macOS Not measured on a current hull.
qemu on macOS Not measured. It takes its network from vmnet, as vz does.
Linux, nerdctl/urunc microVMs Yes. On 2026-09-30, one microVM fetched the other's HTTP marker. On separate isolated networks, the same request timed out while connection controls passed. The ARM run used documented console and runtime setup workarounds. The amd64 run on 2026-10-02 gave the same results with the shipped runtime, unmodified.

On Linux and on hvi, two agents on shared that you gave different credentials can each reach whatever the other listens on. The blast radius in Exposure inside the sandbox is therefore narrow per guest home and per token, not per sandbox.

Warning The hvi answer changed between two measurements, and nothing in Brig noticed at the time. Do not treat any answer about shared in the table as a property of Brig.

If two agents must not reach each other, do this:

Backend Action
hvi or Linux run both with --network isolated
vz or qemu run them on separate hosts. isolated is refused there.

isolated is the guarantee. --network isolated also moves an existing shared sandbox onto it.

isolated gives the sandbox its own network:

Platform The sandbox's own network
Linux its own CNI network
hvi its own gateway process, on a separate /30

No other sandbox is on that network, whatever the backend does with a shared one. A sandbox that carries an egress policy is isolated whether or not it asked to be. The rules live on that gateway and cover everything behind it.

Brig owns no network to give on vz or qemu. It stops a run that asks for isolated there, and names the backend that implements it. Brig does not boot that run onto the shared network.

The guarantee covers reachability, not resources. An isolated sandbox uses one more process and one more network than a shared one. brig rm --all prunes those.

Terminal output #

Brig does not filter what the agent writes to your terminal. brig hands the tty over with syscall.Exec and is gone before the agent produces a byte. That gives correct ^C handling and a true exit status. Every byte the agent emits reaches your terminal emulator unexamined.

An agent that read a hostile README can do any of these:

  • OSC 52 writes to, and reads from, the system clipboard.
  • DCS sequences are forwarded verbatim by tmux and screen to the outer terminal.
  • A cursor-position query makes the terminal type its reply onto your shell's standard input.
Command Filters control sequences?
brig run no
brig run --json no. brig stays alive as the agent's parent so it can print one status line after the agent exits. The tty is still the agent's. brig reads nothing the agent prints and writes nothing to it. The exit status is still the agent's own.
brig logs yes, by default. It reads a log back and does not drive a terminal.
brig logs --raw no. It hands you the bytes with the surface above intact.
hull exec, hull logs yes, because hull stays in the middle of that stream

If this matters for your threat model, run Brig inside a terminal you are willing to lose, or through hull exec.

Guest home contents #

Brig does not protect the guest home from the agent. The agent can write everything in it, because the agent works there.

Spending #

Brig does not stop an agent from spending your money. The deny list only keeps a metered key from being forwarded by accident.

Trust assumptions #

The sections above narrow what an agent can reach. You must still trust four things.

The guest image and its publisher. In every BRIG_VERIFY mode, the image is code that runs with your credentials. Guest images states what each mode verifies and what it boots on a warning.

The profile author. A profile names the image, the volumes it hostmounts, and any files: binding, which the deny list does not cover. A profile is only as careful as its author.

hull or nerdctl. The runtime, not Brig, owns the kernel boundary and every device and namespace decision. It also decides whether the shared network forwards traffic between guests. Brig reports what it resolved. It neither hardens nor weakens what the runtime does. Architecture describes how hvi, the default VMM on macOS, holds that boundary.

Brig itself. Brig runs as you, on the host, outside the sandbox. A resolved credential is plaintext in its process memory for the lifetime of the run. Brig clears that memory on exit, which is defence in depth and not a control. Brig does not claim to protect against a process that already holds your credentials in memory.

Type a command, a flag or an error message.