brig docs

Guides

Networking and egress policy

On this page

You decide where a sandbox can connect. A network posture sets which network the sandbox joins. An egress policy limits the sandbox to the hosts and ranges you allow, for one profile or for one session.

Note Brig enforces an egress policy on hull's hvi backend, on macOS 15 or newer. No other runtime boots a sandbox under a policy it cannot enforce, so a policy is never ignored. --network offline works on every runtime. See Enforcement.

Example policy #

A policy is a YAML file. This one lets the sandbox reach Anthropic's API and one internal range, and nothing else:

apiVersion: brig.sh/v1alpha1
name: locked-down
desc: only Anthropic's API and one internal range
egress:
  default: deny
  allow:
    - host: api.anthropic.com
    - cidr: 10.0.0.0/8
An egress policy at the gateway The sandbox sends every connection through a network gateway that enforces the policy. Connections to api.anthropic.com and 10.0.0.0/8, which the policy allows, pass. A name the policy does not allow does not resolve, and a connection to an address it does not allow does not open. Sandbox claude every connection goes through the gateway Network gateway enforces your policy default: deny allow: - api.anthropic.com - 10.0.0.0/8 api.anthropic.com allowed 10.0.0.0/8 allowed example.com the name does not resolve 203.0.113.7 the connection does not open
The locked-down policy, enforced at the gateway on the hvi backend.
A real session, recorded on macOS. The allowed host connects. The other name does not resolve, and the other address does not connect.

Create it, attach it to a profile, and run the agent:

brig policy create locked-down               # opens the starter in your editor
brig policy attach locked-down claude-code
brig run claude

The sandbox boots behind a gateway that enforces these rules. The document covers the format, and A worked example walks through each verb.

Network postures #

Posture What it permits
shared One network for every sandbox using this posture on the host. Opt-in, except for the vz profiles and retained older sessions
isolated A network of this sandbox's own. The default for new hvi and Linux sandboxes
offline No route out. The agent runs, the guest home is mounted, nothing leaves
brig run claude                         # a new sandbox gets its own network
brig run claude@shared --network shared # explicitly share one with other sandboxes

Both shared and isolated permit internet access. The isolated posture separates sandboxes. It applies no outbound allow list, and it does not promise that host services are unreachable.

Sandboxes on shared reach each other on hvi and on Linux. vz is not measured on a current hull. --network isolated keeps a sandbox off the shared network. Security has the table for each backend and the measurement method.

Setting a posture #

You can set a posture in three places. A higher row overrides a lower row.

Source Example
The --network flag brig run claude --network offline
The BRIG_NETWORK setting BRIG_NETWORK=shared
The profile's network: field network: isolated

Brig refuses a run with an unrecognized value and names the source of the value:

$ BRIG_NETWORK=bogus brig info claude-code
brig: BRIG_NETWORK "bogus" is not a posture: use shared, isolated or offline

Default postures #

Case Default posture How to override
New sandbox on hvi or Linux isolated --network or BRIG_NETWORK
The six built-in hvi profiles isolated, named explicitly in the profile --network or BRIG_NETWORK. On vz or qemu these profiles need --network shared or BRIG_NETWORK=shared
claude-desktop shared. Its GUI requires vz, where Brig cannot give a sandbox its own network --network or BRIG_NETWORK
The unpublished cursor profile Unset in the profile. isolated on Linux and hvi, the shared fallback on vz and qemu --network or BRIG_NETWORK
Custom profile with no network: field, on vz or qemu shared, only when no flag, environment setting or retained posture names a network --network or BRIG_NETWORK. An explicit isolated is an error
Existing sandbox The posture it was started with Name a different posture with --network or BRIG_NETWORK. Brig restarts the sandbox
Sandbox with an egress policy bound isolated, forced Detach the policy

On Linux, nerdctl creates a network per sandbox for isolated.

Changing a posture #

A later command that names no posture, such as brig sh, brig info or a bare brig run, uses the posture the sandbox was started with. The network: field of the profile does not change it.

The posture is fixed at boot. If you name a different posture, Brig restarts the sandbox and names both postures:

$ brig run claude --network shared
brig: this sandbox was started with the isolated posture and --network asks for shared
  ↳ rules are fixed when a sandbox boots, so brig restarts it
  ↳ any other session using this sandbox will be disconnected

Brig records the posture when it boots a sandbox. brig rm removes the record with the sandbox.

Posture in brig info #

brig info prints the posture as one of these three lines:

NETWORK      shared (one network for every sandbox on this host)
NETWORK      isolated (a network of this sandbox's own)
NETWORK      offline (no egress)

The row names the posture of the running sandbox. If the next boot gets a different posture, the row names that posture too. One cause is a policy attach or detach:

NETWORK      isolated (a network of this sandbox's own); shared from its next boot

Postures and policies #

An egress policy bound to a sandbox forces the isolated posture, whether or not --network asks for it.

The record keeps the requested posture and not the forced one. After you detach the policy, the sandbox returns to the recorded posture. Until its next boot, brig info names isolated, the posture the sandbox runs with.

To keep isolated after a detach, pass --network isolated while the policy is attached. Brig restarts the sandbox to record that posture.

Isolation cost #

On hvi, the isolated posture uses one gateway process per sandbox. The measurement in issue 369 shows about 28.7 MB per gateway. That figure is a measurement and not a resource guarantee.

The isolated address pool has 64 networks. If the pool is exhausted, Brig refuses another boot. To free networks, remove unused sandboxes with brig rm <ref>. Linux uses the network allocation of the runtime and not this pool.

The vz and qemu backends

On macOS, isolated needs the hvi backend. vz and qemu take their network from vmnet, which Brig does not own. Brig refuses --network isolated on those backends.

Default postures lists the shared fallback on these backends. The NETWORK row in brig info names that fallback.

Sessions without a posture record

A session from an older version has no posture record. Brig asks the runtime how the sandbox was configured. If Brig recovers that configuration, the recovered posture overrides the default of the profile. A posture read does not write a record or restart the sandbox. A successful run that reuses the sandbox records the recovered posture.

What Brig recovers for an unrecorded hull guest

The socket name sandbox-*.sock, with its case variants, is reserved for isolated gateways. Brig rejects a shared BRIG_GATEWAY_SOCK override with that name before it starts or replaces a networked guest.

Gateway socket of the guest Recovered posture
sandbox-*.sock with a readable, nonempty .spec beside the socket path isolated, even under a previous gateway directory
sandbox-*.sock with a missing, empty or unreadable .spec unknown. A flagless run is refused
Any other gateway name shared

Brig reads the .spec beside the socket path in the saved argv of hull. Only isolated gateways write that file. The current gateway environment does not select the spec that Brig reads.

brig stop removes the spec, and the write of the spec at gateway startup is best effort. A missing spec therefore does not prove shared networking. An older isolated guest that was stopped with no posture record still needs an explicit network choice.

The spec records gateway configuration and not the identity of a VM. Older versions allowed shared overrides named sandbox-*.sock. If such an override reuses an old isolated socket path with a leftover spec, this recovery can wrongly report isolated.

Unknown postures

If Brig cannot establish the posture of an existing sandbox, it refuses a run that names no posture. It does not apply the default for a new sandbox. If no runtime can inspect the sandbox, brig info reports the posture as unknown.

To recreate the sandbox, name the posture with --network or BRIG_NETWORK. This disconnects every session that uses the sandbox. If the runtime confirms that a sandbox is absent, the defaults for a new sandbox apply.

The inspect command of hull cannot distinguish absence from unreadable metadata, and the hull listing omits unreadable records. Brig therefore keeps an indexed legacy session unknown even when hull says "instance not found". You have two remedies:

  • If you removed the VM directly with hull rm, brig ls prunes its stale session entry.
  • You can name the posture explicitly.

For an unexplained error, check runtime access and saved state before you use either remedy.

If all session-index evidence is also lost, hull cannot distinguish that case from a new name. Brig then uses the default for a new sandbox.

Gateway settings

BRIG_GATEWAY_DIR sets where Brig reads networks.json and allocator records. If no gateway directory is set, the directory of BRIG_GATEWAY_SOCK sets it. To find those records after a change, restore the original settings.

If Brig cannot reuse the recovered configuration of a guest, you can name a network explicitly to recreate the guest. Before you recreate a shared guest, choose a shared socket name that is not reserved.

Posture recovery does not rebuild lost allocator or gateway records. If a recovered posture fails the current gateway consistency check, a flagless run refuses to replace the guest. Restore the gateway settings or pass --network explicitly. The normal consistency checks still apply to sandboxes with a saved posture record.

Two releases sharing one sandbox

An older release that boots the sandbox again does not update the record. The record can then name a posture the sandbox does not have, and brig info reports the recorded one. There is one exception. On hvi, if the record says shared and the sandbox is behind an isolated gateway, brig info names isolated.

To write a new record, run brig stop and then brig run from the current release.

Writing a policy #

A policy is a named YAML or JSON document that declares what an agent can reach outbound. It sets a default of allow or deny, and host or cidr exceptions on either side.

brig policy create locked-down   # writes ~/.config/brig/policies/locked-down.yaml
brig policy edit locked-down     # change the rules

Policy files #

Brig keeps one file per policy in a flat directory, for example ~/.config/brig/policies/locked-down.yaml.

Setting Policy directory
Default ~/.config/brig/policies
$XDG_CONFIG_HOME set $XDG_CONFIG_HOME/brig/policies
BRIG_POLICY_DIR set That path, taken as given

An empty or relative $XDG_CONFIG_HOME counts as unset. This follows the XDG Base Directory Specification, version 0.8. Brig does not check that an explicit BRIG_POLICY_DIR is absolute.

The directory starts empty. brig policy create and brig policy edit are the only commands that write policy files to it.

name: in the file overrides the filename, as it does for a profile. create always names the file after the policy. A directory can hold any number of policies.

A file that fails to parse does not stop the other files from loading. brig policy ls reports that file on stderr and lists every policy that loaded. It reports two files that declare the same name in the same way, and it loads neither.

The document #

Example policy shows a complete document.

Field Required What it is
apiVersion yes Pins the document shape. brig.sh/v1alpha1 is the only value this build knows. Anything else is refused
name yes The policy's identifier. See Naming a policy
desc no One line, shown by brig policy ls
egress.default yes allow or deny, applied to any traffic neither list below names
egress.allow no Exceptions to a deny default
egress.deny no A host or range to refuse. For its priority over allow and default, see Limits

Each entry in allow or deny names one of host: or cidr:. Brig refuses an entry with both or with neither.

Rule Value Validation
host: A domain, or a glob such as "*.githubusercontent.com" Refused only for whitespace or a control character
cidr: A network range such as 10.0.0.0/8 Checked with Go's net.ParseCIDR. A typo like 10.0.0/8 is refused

The format sets no glob grammar for host:. The enforcer decides which wildcard forms it accepts. The current gateway matches the glob against the name that the guest asks its resolver for.

Parsing is strict. A document with an unrecognized field fails to parse. Examples are engine:, mode: and a typo such as dsc:. The format has no field that names how a rule is applied.

Naming a policy #

A policy name uses the same characters as a profile name: lowercase letters, digits, dot, dash and underscore. It must start with a letter or digit. Brig checks the name before it builds a path from it, so a bad name never reaches the disk.

One rule applies only to policies. YAML reads an unquoted bare word such as no, true or 123 as a boolean or a number. A policy file that says name: no declares the name false.

brig policy create writes the name the way the starter template writes it, then reads the result back. It refuses a name that does not come back as itself:

$ brig policy create no
brig: name "no" reads as false when written unquoted in YAML, not as itself; pick a different name

The verbs #

Verb What it does
brig policy ls Lists every policy that parses, by name and description. For a bound policy, also lists what binds it
brig policy create <name> Writes a starter document, then opens it: $VISUAL, then $EDITOR, then vi
brig policy edit <name> [--force] Opens an existing policy. Replaces it only if the save still parses and validates. Refuses a rename that orphans anything bound to it, inline or attached, unless --force
brig policy show <name> [--json] Prints the parsed document
brig policy rm <name> [--force] Deletes it. Refuses a policy that is bound to anything, inline or attached, unless --force
brig policy attach <policy> <profile> [-n NAME] Binds it to every run of a profile. With -n, binds it to one session by name instead
brig policy detach <policy> <profile> [-n NAME] Reverses an attach
brig policy check <profile> [-n NAME] Lists what is effectively bound to a run of the profile or the -n session. Reports whether Brig can enforce anything against it at all

Command lines for all eight:

brig policy ls
brig policy create locked-down
brig policy edit locked-down
brig policy show locked-down --json
brig policy attach locked-down claude-code
brig policy detach locked-down claude-code
brig policy check claude-code
brig policy rm locked-down --force

create #

create refuses to overwrite a file at the target path unless you pass --force.

It refuses a name that a different file already declares, with or without --force.

ls #

brig policy ls prints the bindings of a policy under it. A binding is an inline policy: entry, a profile-level attach, or a session-level attach. A session-level attach prints as <profile> -n <session>:

$ brig policy ls
locked-down     only Anthropic's API and one internal range
                bound to: claude-code, claude-code -n refactor

attach and detach #

attach and detach write to attachments.yaml in the policy directory. They do not write to the policy or the profile.

$ brig policy attach locked-down claude-code
attached locked-down to claude-code
note: enforced on the hvi backend, which gives the sandbox a network of its own; a run on any other backend is refused rather than left unenforced
$ brig policy attach locked-down claude-code -n refactor
attached locked-down to claude-code -n refactor
note: enforced on the hvi backend, which gives the sandbox a network of its own; a run on any other backend is refused rather than left unenforced

attach refuses, and writes nothing, in three cases:

  • Either name does not exist.
  • The profile is kind: shell or kind: gui, which has no agent to hook an egress rule into.
  • The profile already declares the policy inline in its policy: list.
$ brig policy attach locked-down ubuntu
brig: cannot attach locked-down to ubuntu: ubuntu is kind: shell, which has no agent to hook an egress rule into. Nothing was written

Both attach and check print the note: line on stderr. The word "attached", or a policy name that check prints, does not mean that a rule is in force.

detach reverses attach:

$ brig policy detach locked-down claude-code -n refactor
detached locked-down from claude-code -n refactor

detach refuses a policy that the profile declares inline. To remove that binding, edit the policy: list of the profile. A -n detach is not refused, because an inline entry and a session-level attach are different bindings.

check #

check resolves all bindings (inline, profile-level and session-level) for one profile, or with -n for one session. It lists the policies that apply and runs the same CheckCoverage refusal as attach:

$ brig policy check claude-code
locked-down
note: enforced on the hvi backend, which gives the sandbox a network of its own; a run on any other backend is refused rather than left unenforced
$ brig policy check ubuntu
no policy applies to ubuntu
brig: cannot enforce any policy on ubuntu: ubuntu is kind: shell, which has no agent to hook an egress rule into

check makes two structural checks:

  • Whether the profile is kind: shell or kind: gui. Neither can enforce a policy.
  • Whether every bound name still resolves to a policy that loaded.

Note check does not resolve the current runtime, the current hypervisor, or the version of the runtime. It cannot tell you whether the host you are on will boot the run or refuse it. Enforcement describes the checks that answer that.

--force on rm or on a rename can leave a binding to a name that no policy loads under:

$ brig policy rm locked-down --force
removed /home/you/.config/brig/policies/locked-down.yaml
$ brig policy check claude-code
locked-down (not loaded)
brig: claude-code is bound to locked-down, which no policy loads under -- nothing can enforce what did not load

rm #

With --force, rm removes the file of a bound policy, and each binding then points at nothing:

$ brig policy rm locked-down
brig: locked-down is bound to claude-code. Detach it first, or pass --force to remove it anyway
$ brig policy rm locked-down --force
removed /home/you/.config/brig/policies/locked-down.yaml

The refusal says "detach it" for an attach, and "edit the profile's policy: list" for an inline entry. It says both if a policy is bound both ways.

edit #

edit opens a scratch copy. If the copy parses and validates, Brig replaces the original through a temp file and a rename in the same directory. A crash or a full disk during the write cannot leave the real file half written. If the copy fails, the original stays unchanged:

$ brig policy edit locked-down
brig: not saved, /home/you/.config/brig/policies/locked-down.yaml is unchanged: cidr "10.0.0/8" is not a valid CIDR: invalid CIDR address: 10.0.0/8
your edit is still at /tmp/brig-policy-edit-2427992151.yaml

A rename is a change to name:. If the old name is bound to anything, Brig refuses the rename in the same way:

$ brig policy edit locked-down
brig: not saved, /home/you/.config/brig/policies/locked-down.yaml is unchanged: renaming locked-down to totally-new would leave claude-code pointing at a name nothing declares. Detach it first, or pass --force to rename it anyway
your edit is still at /tmp/brig-policy-edit-2427992151.yaml

A save that keeps the same name never triggers this check.

Session bindings #

-n NAME must be the slug form of the session name: lowercase letters, digits, dot, dash and underscore. Brig refuses attach -n Refactor and names the slug to use.

The <agent>@<label> form follows the same slug rule. Brig accepts claude@refactor and refuses claude@Refactor.

The slug names the sandbox and the workspace, keys the session index, and selects the policy.

brig policy check -n reads the name the same way, so it reports what the run gets. Unlike attach -n, it accepts a name that is not a slug. A hand edit or an earlier build can leave such a key, and brig policy ls prints it. check reports a row under that key and says that no run reaches it:

$ brig policy check claude-code -n "My Work"
brig: no-net is recorded under "My Work", which no run reaches
  ↳ a session named "My Work" starts "my-work"
  → to remove it:  brig policy detach no-net claude-code -n "My Work"
no-net
note: enforced on the hvi backend, which gives the sandbox a network of its own; a run on any other backend is refused rather than left unenforced

One gap remains. attach -n refuses a name that any reserved profile ends in. A session is refused only a name that is reserved for its agent. claude@desktop therefore opens an ordinary session. Brig refuses brig policy attach locked-down claude-code -n desktop, because the name collides with claude-desktop. You cannot bind a policy to that one session.

Sessions named with --name

brig run claude --name Refactor sanitizes Refactor to the slug refactor for the sandbox and the workspace. It reports the directory it used. Brig looks up the policy under that slug too. brig policy attach locked-down claude-code -n refactor covers the session, whichever way you named it.

A worked example #

Start from nothing:

$ brig policy ls
no policies yet; your own live in /home/you/.config/brig/policies
brig policy create <name> writes a starter one

Create a policy. The starter opens in your editor. In this example, the starter is already filled in:

$ brig policy create locked-down
/home/you/.config/brig/policies/locked-down.yaml created
$ brig policy ls
locked-down     only Anthropic's API and one internal range

Show it, as YAML or as JSON:

$ brig policy show locked-down
apiVersion: brig.sh/v1alpha1
desc: only Anthropic's API and one internal range
egress:
  allow:
  - host: api.anthropic.com
  - cidr: 10.0.0.0/8
  default: deny
name: locked-down
$ brig policy show locked-down --json
{
  "apiVersion": "brig.sh/v1alpha1",
  "name": "locked-down",
  "desc": "only Anthropic's API and one internal range",
  "egress": {
    "default": "deny",
    "allow": [
      { "host": "api.anthropic.com" },
      { "cidr": "10.0.0.0/8" }
    ]
  }
}

show prints the parsed document and not the file verbatim. The YAML output has sorted keys, so the field order differs from the file.

Edit it, and remove it:

$ brig policy edit locked-down
/home/you/.config/brig/policies/locked-down.yaml updated
$ brig policy rm locked-down
removed /home/you/.config/brig/policies/locked-down.yaml

Common errors #

What Brig says What happened
unknown policy "x". `brig policy ls` lists them show, edit or rm on a name that is not there
name "x" may use only lowercase letters, digits, dot, dash and underscore, and must start with a letter or digit See Naming a policy
name "x" reads as false when written unquoted in YAML, not as itself; pick a different name The name is a bare YAML boolean, null or number word. See Naming a policy
<path> already exists. Edit it directly with `brig policy edit x`, or pass --force to replace it with a fresh starter create on a name whose file is already there
policy "x" already exists, declared in <path>. Edit it directly with `brig policy edit x`, or remove that file first create on a name a different file already declares. --force does not help here
a rule needs host: or cidr: A rule in allow: or deny: named neither
a rule takes host: or cidr:, not both … A rule named both
cidr "x" is not a valid CIDR: … A typo in a cidr: value, such as a missing octet
host "x" contains whitespace or a control character A host: value that cannot be a domain or glob under any grammar
apiVersion is required, and must be "brig.sh/v1alpha1" A document with no apiVersion:, or the wrong one
not saved, <path> is unchanged: … The save from edit did not parse or validate, or it renamed a bound policy without --force. The real file is untouched. The error names where your edit still is
unknown profile "x". `brig agent ls` lists them attach, detach or check naming a profile that is not there
cannot attach x to y: y is kind: shell, which has no agent to hook an egress rule into. Nothing was written attach to a kind: shell or kind: gui profile
cannot enforce any policy on x: x is kind: shell, which has no agent to hook an egress rule into check on a kind: shell or kind: gui profile
x is bound to y, which no policy loads under -- nothing can enforce what did not load check on a profile bound to a name nothing loads under. Either --force on rm or a rename left no policy behind it, or the file that declares it did not parse (named separately on stderr)
x is already declared inline in y's policy: list, which binds every run already. Nothing was written attach naming a policy the profile's own policy: list already declares
x is declared inline in y's policy: list, not attached; edit the profile directly to remove it detach naming a policy the profile's own policy: list declares, without -n
x is bound to y. Detach it first, or pass --force to remove it anyway rm on a policy attached to a profile or a session. A policy declared only inline says "edit the profile's policy: list" instead
a policy applies to this sandbox, and hull on vz cannot enforce the egress policy: … A policy on a run path whose answer is cannot enforce. See Enforcement
a policy applies to this sandbox, and hull on hvi cannot enforce the egress policy: the network-gateway of <bin> has no --egress-default. Upgrade the runtime, or detach the policy The runtime is older than the hull that added the --egress-* gateway flags
a policy applies to this sandbox, and whether hull on hvi enforces the egress policy is unknown: the probe `<bin> network-gateway --help` failed: … The probe of the runtime did not run, exited non-zero, or gave no answer within 30 seconds
a policy applies to this sandbox, and whether hull on krun enforces the egress policy is unknown: brig holds no answer for this run path. Run it on hull's hvi backend (BRIG_HYPERVISOR=hvi), or detach the policy BRIG_HYPERVISOR names a backend Brig holds no record for, such as krun. Brig refuses the run

Default egress #

With no policy attached, a sandbox has open internet access, so the agent reaches its API with no setup. To limit it, attach a policy.

No profile that Brig ships binds a policy, so brig run <agent> on a fresh install filters nothing. The test TestNoShippedProfileBindsAPolicy holds that in place. No gateway gets a rule until you attach a policy to that profile or that session.

No policy and a deny default are different cases:

What you do Result
Attach no policy No filtering
Attach a policy whose default: is deny Everything is refused except what its allow list names. With an empty allow list, the sandbox has no way out

Enforcement #

One backend can enforce an egress policy: hull's hvi backend. It enforces the policy at the user-mode network gateway that Brig gives the sandbox.

Brig holds one answer for each run path, which is a runtime with one backend. The answer comes from one table in internal/runtime/capability.go.

Run path Egress policy Why
hull on hvi enforced The rules go on the gateway that is the sandbox's only way out. The gateway probe confirms it before Brig starts that gateway
hull on vz cannot enforce vmnet, which Brig does not filter
hull on qemu cannot enforce vmnet, which Brig does not filter
nerdctl or docker, on any shim cannot enforce Nothing reads the rules into the run, and the container network is not filtered
hull on any other backend unknown Brig holds no answer for it

Brig boots a run with a bound policy only on enforced. On cannot enforce or unknown, it refuses the boot. The refusal names the property, the runtime and the backend. On vz, qemu and every Linux runtime, the refusal also names the backend that enforces.

Brig never refuses two kinds of run:

  • A run with no policy.
  • A run under --network offline, on any backend. A sandbox with no route out satisfies every rule set.

Brig makes this check before anything starts. It makes the check again when it finds the sandbox already running, so a running sandbox does not bypass it.

Warning Every runtime that Brig ships refuses a policy it cannot enforce. This guarantee covers the runtimes that Brig ships today. It is not a property of the interface. A runtime that never answers the question is never asked, and is not refused.

On hvi #

At boot, Brig reads every policy bound to the run. It puts the rules on the network gateway that it gives the sandbox.

A measurement in a real guest gave three results. An allowed name is reachable, a denied name does not resolve, and an address dialled directly does not connect. See the egress policy test record.

The gateway probe #

The hull binary must have the gateway's --egress-* flags. Brig probes the binary with <bin> network-gateway --help. It does not check a version number. On hvi, the probe confirms or overturns the answer from the table.

Probe result Answer What Brig does
The help text lists --egress-default and the probe exits zero enforced Boots the run
The help text has no --egress-default cannot enforce Refuses the boot and names the binary
The binary does not run unknown Refuses the boot
A non-zero exit, even when the help text lists --egress-default unknown Refuses the boot
No answer within 30 seconds unknown Refuses the boot

A refusal after a failed probe names the binary, the probe command and its error.

The refusal for a hull without the flags:

$ brig policy attach locked-down claude-code
attached locked-down to claude-code
note: enforced on the hvi backend, which gives the sandbox a network of its own; a run on any other backend is refused rather than left unenforced
$ brig run claude
brig: a policy applies to this sandbox, and hull on hvi cannot enforce the egress policy: the network-gateway of /opt/homebrew/bin/hull has no --egress-default. Upgrade the runtime, or detach the policy. brig will not boot a sandbox under a policy nothing enforces

Binding properties #

The rules are fixed when the sandbox boots. They go on the command line of the gateway, which reads them once. An edit to a policy changes what the next boot enforces. No environment variable overrides the rules of a running gateway.

A sandbox that is up when the rules change does not continue under the old rules. Brig detects the mismatch, then stops, removes and reboots the sandbox. It warns that any other session on the sandbox will be disconnected.

A policy takes the network posture with it. Rules belong to a gateway and cover every member of its network. A sandbox with rules therefore gets its own network, as Postures and policies describes. The NETWORK row of the execution envelope shows the isolated posture. The forced posture only narrows what was asked for.

Several policies at once are unioned. A rule in any bound policy is a rule of the run. The default is the strictest that any policy names: one deny makes the run deny-by-default. A host that the second policy allows is reachable even when the first policy alone denies it. The deny priority in Limits applies across the whole set.

Limits #

attach and check do not inspect the rules in a policy. They cannot tell you that an allow glob matches nothing you intended. They tell you only that the document parses.

Rule priority. Brig applies no priority when it merges policies. It concatenates the allow and deny lists from every bound policy. Each rule reaches the gateway as an --egress-allow or --egress-deny flag on its command line. At the gateway, deny takes priority over allow and over default. That order is the documented behavior of the gateway. Brig does not check it.

What was measured. The one measurement in the repository is the egress policy test record. It covers a default: deny policy with one host allow. It does not cover a deny rule that overrides an allow rule, or a default: allow policy.

Host rules. The gateway enforces a host rule through its resolver, so the coverage of the rule depends on the default. This is also the documented behavior of the gateway.

Policy default What a host rule covers
default: deny The resolver answers only names an allow glob covers. The guest reaches nothing it did not resolve there. Traffic sent straight to an address, DNS over HTTPS and DNS over TLS do not get out
default: allow A host deny is best effort, because traffic sent straight to an address never asks for a name

A cidr rule is matched on the address with either default.

Type a command, a flag or an error message.