Retrieved article excerpt
Open article · Retrieved 2026-09-15T19:22:04.861944+00:00
# What Brig protects, and what it does not
An agent runs code you did not write. You run it in a sandbox because that
code executes against a machine that holds everything you have. Brig narrows
what "everything" means. On the host, the agent can reach its guest home and
the credentials you gave it. It can also reach any project you name on the
run line. No other host directory is mounted.
No host credential source is read on the run path: the only store a run
opens is Brig's own. See [secrets.md](https://github.com/brig-sh/brig/blob/main/docs/secrets.md). What the agent can reach
over the network is a separate question, with a much weaker answer, covered
below.
If you have found a flaw in one of these boundaries, [SECURITY.md](https://github.com/brig-sh/brig/blob/main/SECURITY.md)
is how to report it privately. The section below on
[things brig does not claim](https://github.com/brig-sh/brig/blob/main/docs/security.md#things-brig-does-not-claim) is the line between a
vulnerability and a known limitation.
## 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>`
- the credentials you deliver to it
- the internet, on the default `shared` network
- any hostmount volume a profile declares
The guest cannot access:
- any other host directory
- your keychain
- your SSH agent
- your secret manager
- an environment variable a profile's `deny` list refuses
Mounting a project or delivering a credential changes both lists. The agent
can change the real files at those two mounts, not a copy of them. Anything
it can read there it can also send over the network. A credential you deliver
is usable by the agent for as long as it holds it. The agent can misuse it
the same way a person holding it can.
Every hostmount in a shipped profile already lives inside the guest home.
That means no shipped hostmount exposes anything today, a property of the
shipped profiles rather than a guarantee about a profile you write yourself.
A `deny` list only ever covers an environment variable. A `files:` binding
reaches the guest by a different channel, and the list does not see it.
What "the internet" means in practice is in
[Things brig does not claim](https://github.com/brig-sh/brig/blob/main/docs/security.md#things-brig-does-not-claim) below. So is
whether the guest can reach a service bound on the host.
## The boundary
The sandbox is a microVM on both macOS and Linux. On macOS it is booted by
[hull](https://github.com/brig-sh/hull) over Virtualization.framework, which
`brew install --cask brig` brings along. On Linux Brig drives `nerdctl` and
hands the container to the urunc shim (`io.containerd.urunc.v2`), which is the
default rather than the direction. That gives the guest a kernel of its own
there too. `BRIG_CONTAINERD_RUNTIME=runc` asks for a plain container instead, which
shares the host kernel. That is the weaker of the two, and it is something you
have to choose rather than something you get.
Which of them you got is the `ISOLATION` row of the execution envelope, printed
before every boot and by `brig info`:
```
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 row reports what this run resolved: the binary in hand, the backend it
settled on, and the shim it will name. That is not the same as what the
paragraph above promises. Brig may not recognise a shim but could still use it
to boot a sandbox, and Brig cannot establish the isolation that sandbox gets from a
shim name alone. So the row says it cannot tell, instead of claiming the
stronger boundary.
Inside Brig, the guest has your guest home mounted as its home, read-write. Name
a project on the run line and that project is a second host directory, also
mounted read-write, at `/work/<name>`. The agent can change those files too.
Each hostmount volume in the profile is an additional share. Every hostmount volume in
a shipped profile lives inside the guest home already, so nothing extra is exposed
today. That is a property of the shipped profiles, not a guarantee. A
hostmount your own profile declares outside a tmpfs cover is a host path the
guest can see.
Beyond those, the guest does not have your keychain, your SSH agent, your
secret manager, or any other directory on the host. That inaccessibility is
the isolation boundary for everything else. It is also the reason credentials
have to be forwarded in explicitly: the guest cannot fetch them for itself.
## Credentials
**A run reads no host credential source.** Nothing on the
`brig run`, `exec` or `shell` path reaches a keychain item Brig did not write.
Nothing on that path reaches a credential file outside the guest home, or a
host command that produces one. Two host reads do happen, and no setting
turns either off. Brig runs `git config --get` in the directory you invoked it from, for `user.name`,
`user.email` and `github.user`, to resolve the commit identity forwarded into
the guest. If `github.user` comes back empty, it then reads the `user:` line
from the stanza for your git host in gh's `hosts.yml`, under `$GH_CONFIG_DIR`
or `~/.config/gh`. That line names the login that pairs with the forwarded
token. That file usually carries gh's own OAuth token as well. Brig takes the login and
nothing else. `BRIG_GIT_IDENTITY=0`, `BRIG_GIT_CONFIG=0` and `BRIG_GIT_USER`
change what Brig does with the answers, not whether it asks. Your host login
enters Brig's own store once, when you type `brig secret import <profile>`,
and every run afterwards 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
```
A credential reaches the guest by one of two channels, and the profile picks
per secret. `files:` writes it 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 still reads the named variable
from its own environment. Whatever populates that environment remains a
usable backend for those.
On Linux the store is a Secret Service keyring on your session bus,
gnome-keyring or KWallet ([secrets.md](https://github.com/brig-sh/brig/blob/main/docs/secrets.md#linux)). A host with no
keyring has no store. A profile whose secrets are *optional* degrades there
rather than failing: the run boots and the agent asks for a login. That is
what `claude-code` does. A **required** secret does fail outright,
because there is nowhere on that host to read it from.
Values are re-read on every exec, so a rotated credential is picked up without
restarting the sandbox. Nothing is written into the guest home from the host
for this.
### What file delivery buys, and what it costs
A credential delivered as a file stays out of `/proc/<pid>/environ` and is not
inherited by processes the agent spawns. It can also be rewritten under a
running agent, so a rotated secret can reach a live session, which no
environment variable can. The bytes land on a memory-backed mount covering
the agent's whole config directory, verified to be `tmpfs` with no swap
before anything is written. So there is no path from the credential to your
disk to check.
Weigh these costs before you rely on file delivery. None of them is small
enough to leave implied.
- **Brig stores and hands over a refresh token.** There is no way to give
Claude Code a working `.credentials.json` without `refreshToken` and
`refreshTokenExpiresAt`. A file carrying only an access token is *worse*
than an environment variable, because the agent attempts a refresh, fails,
and prompts. So a compromised agent inside the sandbox can mint access
tokens indefinitely, and keeps doing so after the host's own token has
expired. The compensating argument is real and belongs beside it: the guest
refreshes for itself, so a long session stops breaking every few hours. It
is a trade, taken deliberately.
- **Brig's copy is less protected than the item it came from.** The host's
Claude item is ACL-scoped to the application that wrote it. That is why it
raises a dialog the first time something else reads it. The copy Brig
writes carries the default ACL, the same one every secret in
[the secret store](https://github.com/brig-sh/brig/blob/main/docs/security.md#the-secret-store) carries. The same consequence
applies: keeping the stored copy low-value is the only real mitigation, and
a refresh token is not low-value. When you are not using it, delete it:
`brig secret delete claude-credentials`.
- **The denylist stays env-scoped.** A `files:` binding bypasses the deny check
entirely, and no name check can fix that. A profile can deliver a metered
API key inside a `settings.json`, and nothing sees it. That is defensible
rather than a hole. `deny` exists to catch **accident**, an ambient
variable swept into the guest because it happened to be in your shell. A file
binding takes an explicit stored secret and an explicit binding written by
the profile author. Nobody file-binds an `ANTHROPIC_API_KEY` by mistake. The
two names on `claude-code`'s denylist are env-shaped by the agent's own
design, so the guard still covers the channel the risk uses.
- **The stored copy does not rotate.** A credential renewed on the host does
not update Brig's copy. One *revoked* on the host stays valid in Brig's
store until you re-import or delete it. Brig warns before boot when the
stored copy has expired, and names the command that refreshes it. It cannot
see a revocation at all.
A `0600` file is also still readable by anything running as the agent's uid
inside the sandbox. Files narrow the exposure. They do not draw a boundary.
See [what is still exposed](https://github.com/brig-sh/brig/blob/main/docs/security.md#what-is-still-exposed) below.
Brig applies these rules when it resolves a secret:
- Unset or empty is skipped, so it cannot shadow a value baked into the image.
- A `scheme://` value read from the environment is refused as an unresolved
secret-manager reference. direnv and friends leave those in the environment
readily. Forwarded verbatim, it yields "Invalid username or token" in
the guest, which looks exactly like a broken sandbox. A `value:` literal or
a value from Brig's own secret store skips this check. Brig put it there
on purpose, not left behind by a tool that never resolved it. Refusing it
rejects a perfectly good credential for merely looking like one it is not.
`BRIG_ALLOW_REFS=1` forwards an ambient reference anyway.
- A variable on the profile's `deny` list is refused, with the reason.
`brig info <agent>` reports the guest's environment, by name, and fails the
same way a run does if a declared secret cannot be resolved. It never
prints a value: a secret-sourced variable comes back annotated, for example
`GH_TOKEN(secret)`, never with the value itself. A credential delivered as a
file is not an environment variable and does not appear in that list at all.
### What reaches host disk
The credential file is the part that does not. It lands on a `tmpfs` mount
covering `~/.claude`, checked to be `tmpfs` with no swap before anything is
written. So `~/.claude/.credentials.json` and the temp file the agent
renames onto it never touch your disk. `brig stop` takes that mount with the sandbox,
which is why an in-sandbox login on this profile does not outlive a stop.
The rest of `~/.claude` is not on that mount. Seven paths under it are
hostmounted, so they live in the guest home on host disk and persist across
boots:
- `settings.json` and `CLAUDE.md`, your permission allowlist and your
user-level memory, written by hand or by the agent on your instruc