Guides
Built-in profiles
Brig ships 8 profiles inside the binary. brig agent ls lists them on your machine, and brig agent show <agent> prints one.
Each spec below is the file in internal/profile/specs at the commit this site was built from. The comments in each file say why a field has the value it has. Agent profiles explains the format and how to write your own.
| Profile | Runs | Image |
|---|---|---|
claude-code |
Claude Code (Anthropic) | ghcr.io/brig-sh/claude-code-stock:root |
claude-desktop |
Claude Desktop (Anthropic), in a graphical window | ghcr.io/nofireai/urunc-claude-desktop:aarch64 |
codex |
Codex (OpenAI) | ghcr.io/brig-sh/codex-stock:root |
cursor |
Cursor Agent -- example profile, image unpublished pending a terms check | ghcr.io/brig-sh/cursor:latest |
gemini |
Gemini CLI (Google) -- example profile | ghcr.io/brig-sh/gemini-stock:root |
grok |
Grok CLI (xAI) -- example profile | ghcr.io/brig-sh/grok-stock:root |
opencode |
opencode (OSS, provider-agnostic) -- example profile | ghcr.io/brig-sh/opencode-stock:root |
ubuntu |
A plain Ubuntu shell, running as root | docker.io/library/ubuntu:latest |
claude-code #
Claude Code (Anthropic). Source: claude-code.yaml.
name: claude-code
desc: Claude Code (Anthropic)
binary: claude
image: ghcr.io/brig-sh/claude-code-stock:root
# Boots as an ordinary OCI image: the image carries no kernel, so the
# runtime supplies one. hvi is the backend that path is built around.
hypervisor: hvi
network: isolated
genericBoot: true
# The root account, which is what makes this work on a rootless Linux install:
# rootlesskit maps container uid 0 to the invoking user, so a root guest IS
# that user -- it opens /dev/kvm and owns the workspace it writes. A guest on
# 501 or 1000 lands on a subuid that owns nothing. It costs nothing on macOS,
# where hvi presents the host user's files as uid 0 by default.
#
# brig takes the guest account from the last element of this path.
guestHome: /root
# ~/.claude/skills and ~/.claude/plugins are copied into the guest under
# --skills. Memory is deliberately absent: it is keyed by host project path
# (~/.claude/projects/<slug>/memory), and that slug does not exist in the
# guest, so projecting it would put the files somewhere the agent never looks.
hostConfigDir: ~/.claude
projectPaths: [skills, plugins]
# What this profile needs out of brig's own store, and whether a run without
# it should stop.
#
# Both optional, deliberately. Claude Code completes its own login inside the
# sandbox when nothing supplies a credential -- which is what a host that has
# never run `claude` has always done -- and git reports its own auth error
# without a token. Requiring either would turn both into a refused boot.
#
# sources: are tried in order and the first that exists wins, which is what
# makes this portable without a per-platform predicate. gh-token has none: it
# is hand-created, because `ref: env.GH_TOKEN` below reads the variable LIVE on
# every run while a source would copy it into the keychain once, at import --
# same word, opposite temporal semantics, and the snapshot is the one that goes
# stale without saying so.
secrets:
- name: claude-credentials
required: false
expiryField: expiresAt # found at any depth; drives the stale warning
sources:
- from: keychain
service: Claude Code-credentials # macOS keeps it here
- from: file
path: ~/.claude/.credentials.json # the documented Linux location
hint: run `claude` on the host once to log in
- name: gh-token
required: false
hint: "export GH_TOKEN before running brig, or store one: gh auth token | brig secret create gh-token"
# The whole credential document, byte for byte, at the path Claude Code reads.
#
# Verbatim rather than field-by-field: the host keychain blob IS the format
# .credentials.json takes -- {"claudeAiOauth": {accessToken, refreshToken,
# expiresAt, refreshTokenExpiresAt, scopes[], subscriptionType, rateLimitTier}}
# -- so nothing needs extracting and no field brig does not understand is lost.
#
# The guest is Linux and has no keychain, so this file is the agent's only
# credential store there. File delivery is the native shape, not a workaround
# for the environment variable.
#
# That includes the refresh token, and this is the part worth saying out loud,
# because an earlier design deliberately withheld it: an access token expires
# on its own, while a refresh token mints new ones until the grant is revoked.
# So the guest holds durable account access rather than one session's worth.
#
# Kept anyway, and the reason is the lifetimes. Measured 2026-08-19 on a live
# credential: the access token had 2.9 hours left and the refresh token 98.
# Delivering only the access token would give the sandbox a credential that
# dies within hours, with no way to renew it in the guest -- so every long run
# would stop mid-work and wait for someone to import again on the host. The
# blast radius we accept in exchange is one workspace and whatever the guest
# can reach, which is the same trade the fine-grained GH_TOKEN advice makes.
#
# Two things this rested on and no longer has to. Both measured 2026-08-19,
# in the guest, against a real account:
#
# - The agent DOES refresh in place. With the access token corrupted and the
# refresh token intact, it recovered and rewrote this file. With the
# refresh token removed it could not: "401 OAuth access token is invalid",
# and the file was left untouched. So the refresh token is what buys a
# sandbox more than one access token's worth of life, and delivering only
# the access token would strand a run when that token expired.
# - The provider rotates the refresh token single-use. The refresh above
# invalidated the copy on the host, which needed a fresh login -- and the
# document the guest wrote carries a NEW refresh token that only the guest
# has. So a refresh in the sandbox costs the host its login.
#
# That last point has a consequence this profile cannot fix on its own, and it
# is worth knowing before trusting a long run: brig re-delivers the stored
# credential on every exec, so once a guest has refreshed, the next brig
# command overwrites the guest's new token with the stored one -- which the
# rotation just invalidated. The sandbox then holds a dead credential, and the
# fix is `brig secret import` on the host after a fresh login there.
#
# It is an ordinary file, written into the tmpfs declared under volumes: below.
# NOT bind-mounted onto its own path: measured 2026-08-18, Claude Code writes
# this file atomically (temp file + renameat), and renameat onto a mountpoint
# returns EBUSY -- so a bind mount there breaks in-guest refresh, and the temp
# file lands on the workspace, which is host disk.
files:
- ref: secrets.claude-credentials
path: .claude/.credentials.json
mode: "0600" # measured: what claude itself writes
# What reaches host disk, and what does not.
#
# .claude is tmpfs, so the credential above -- and the temp file the agent
# renames onto it -- never touch the workspace. That is fail-closed by
# construction rather than by inspection: there is no path to the host to
# check.
#
# The hostmounts are the exceptions: state worth keeping across boots, and the
# list is what was actually in a live workspace rather than a guess. sessions,
# projects and history.jsonl are the conversation; plugins and skills are also
# where --skills copies your own, so leaving either off would make that flag
# silently do nothing.
#
# What a live workspace also held and does NOT get one, so these are decisions
# rather than omissions: cache/, .last-update-result.json, policy-limits.json
# and remote-settings.json are all re-fetched, and backups/ holds snapshots of
# .claude.json -- which sits BESIDE .claude rather than inside it, so it
# persists on its own and the snapshots are derived from something that
# survives.
#
# settings.json and CLAUDE.md are the user's own configuration -- the
# permission allowlist and the user-level memory -- written by hand or by the
# agent on the user's instruction, not re-fetched from anywhere. Without a
# hostmount the agent re-asks for permissions it was already granted on every
# boot, so they are exceptions for the same reason the conversation is.
#
# Anything under .claude NOT named here is ephemeral, including anything a
# future Claude Code version starts writing -- which is the cost of this design
# and is documented rather than hidden.
#
# size: is above 64m because this mount is an agent's home rather than a
# config directory: file-history grows with every edit and per-job scratch
# lands here too, and exhausting it surfaces as ENOSPC from the agent's own
# tools with nothing on the host watching.
#
# Order here is taste: brig mounts parents before children by path depth, so a
# hostmount listed above its tmpfs cannot silently lose the state it names.
volumes:
- kind: tmpfs
path: .claude
size: 512m
- kind: hostmount
path: .claude/settings.json
file: true
- kind: hostmount
path: .claude/CLAUDE.md
file: true
- kind: hostmount
path: .claude/sessions
- kind: hostmount
path: .claude/projects
- kind: hostmount
path: .claude/plugins
- kind: hostmount
path: .claude/skills
- kind: hostmount
path: .claude/history.jsonl
file: true
# GH_TOKEN travels as environment because brig's own git credential helper
# reads it there (internal/wrap/git.go: `[ -n "${GH_TOKEN:-}" ] || exit 0`), so
# moving it to a file means changing the helper first. Worth doing later -- the
# helper runs at credential-query time, so a file would let a rotated token
# reach a running agent -- but it is a code change, not a profile change.
#
# The chain is not decoration: env. first keeps `GH_TOKEN=$(gh auth token) brig
# run claude-code` working exactly as it always has, because a bound name is
# dropped from the ambient forward. secrets. second is the fallback for a shell
# that exports nothing.
#
# For git over HTTPS, not for the agent.
#
# IS_SANDBOX is load-bearing on root rather than informational. Claude Code
# refuses --dangerously-skip-permissions outright when it is running as root --
# "cannot be used with root/sudo privileges for security reasons" -- and this
# is what says the boundary is the VM rather than the account. Measured: with
# it unset the flag is refused, with it set the run proceeds.
env:
- name: IS_SANDBOX
value: "1"
- name: GH_TOKEN
refs: [env.GH_TOKEN, secrets.gh-token]
# Both outrank Claude Code's subscription credential in its auth precedence, so
# forwarding either would move the sandbox onto metered API billing without
# saying so.
#
# The denylist guards the environment channel, and that is the channel that
# matters: these two are env-shaped by the agent's own design, and the risk it
# exists for is an ambient variable being swept in by accident. A files:
# binding cannot be an accident -- it takes an explicit stored secret and an
# explicit binding written by the profile author.
deny: [ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN]
# Kept, and it now means what it always said. brig never writes to this host
# path -- the credential lives in the tmpfs above -- so a .credentials.json
# found in the workspace is a real token left on disk by an older brig or an
# older workspace. It is the warning this migration most needs.
staleCredentialFiles: [.claude/.credentials.json]
# statePaths is gone: volumes: above is the same list made load-bearing, and
# two lists that can disagree about what persists is the failure worth
# removing. .claude.json is not in it because it sits beside .claude rather
# than inside it, so no tmpfs covers it and the workspace keeps it as it always
# did.
headless: true
mem: 4096
cpus: 4
# Not authentication: a first-run screen the guest cannot complete, because
# choosing a login method there opens a browser the microVM does not have.
# Never a credential -- those go through files: and env: above.
onboarding:
file: .claude.json
seed:
hasCompletedOnboarding: true
hasTrustDialogAccepted: true
trustKey: [projects, hasTrustDialogAccepted]claude-desktop #
Claude Desktop (Anthropic), in a graphical window. Source: claude-desktop.yaml.
name: claude-desktop
desc: Claude Desktop (Anthropic), in a graphical window
# kind: gui -- the app owns the console, so there is no binary to exec and
# nothing to pass arguments to.
kind: gui
guiTitle: Claude Desktop
image: ghcr.io/nofireai/urunc-claude-desktop:aarch64
# The graphical console uses vz, which cannot provide isolated networking.
network: shared
guestHome: /home/claude
# The same secret as claude-code, with the same sources, written out in full.
#
# Names are flat and global -- claude-code and claude-desktop are the same
# login -- so `brig secret import claude-code` fills this one too, and says so.
# The five lines are duplicated rather than referenced on purpose: declaring
# the secret with no sources and leaning on the other profile would mean a
# missing-secret hint that searches every profile's importers for one covering
# the name, which is a lot of machinery to avoid five lines. Duplicated, each
# profile imports standalone and neither depends on the other having been used.
secrets:
- name: claude-credentials
required: false
expiryField: expiresAt # found at any depth; drives the stale warning
sources:
- from: keychain
service: Claude Code-credentials # macOS keeps it here
- from: file
path: ~/.claude/.credentials.json # the documented Linux location
hint: run `claude` on the host once to log in
- name: gh-token
required: false
hint: "export GH_TOKEN before running brig, or store one: gh auth token | brig secret create gh-token"
# The bundled Claude Code reads this document, at the path it reads it from.
# Written into the tmpfs below as an ordinary file -- see claude-code.yaml for
# why it is not a bind mount on its own path.
files:
- ref: secrets.claude-credentials
path: .claude/.credentials.json
mode: "0600"
# .claude is tmpfs, so the credential above cannot reach host disk. The
# hostmounts name the exceptions worth keeping across boots.
#
# .config/Claude -- the Electron app's own profile, which is where the desktop
# app keeps its window state and its logged-in session -- is deliberately NOT
# here. It sits outside .claude, so no tmpfs covers it and the workspace keeps
# it exactly as it always did; a hostmount under no tmpfs would mount the
# workspace onto itself and is refused at parse time.
volumes:
- kind: tmpfs
path: .claude
size: 512m
- kind: hostmount
path: .claude/settings.json
file: true
- kind: hostmount
path: .claude/CLAUDE.md
file: true
- kind: hostmount
path: .claude/sessions
- kind: hostmount
path: .claude/projects
- kind: hostmount
path: .claude/plugins
- kind: hostmount
path: .claude/history.jsonl
file: true
# For git over HTTPS, not for the app. env. first so an exported GH_TOKEN keeps
# reaching the guest exactly as it did before this profile bound the name.
env:
- name: GH_TOKEN
refs: [env.GH_TOKEN, secrets.gh-token]
deny: [ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN]
# brig never writes to this host path -- the credential lives in the tmpfs
# above -- so one found in the workspace is a real token left by an older brig.
staleCredentialFiles: [.claude/.credentials.json]
mem: 6144
cpus: 4
# This profile owns ~/brig/claude-desktop, so a session called "desktop" is
# refused rather than landing a Claude Code session on the Desktop app's
# workspace.
reserved: truecodex #
Codex (OpenAI). Source: codex.yaml.
name: codex
desc: Codex (OpenAI)
binary: codex
image: ghcr.io/brig-sh/codex-stock:root
# Boots as an ordinary OCI image: the image carries no kernel, so the
# runtime supplies one. hvi is the backend that path is built around.
hypervisor: hvi
network: isolated
genericBoot: true
# The root account, which is what makes this work on a rootless Linux install:
# rootlesskit maps container uid 0 to the invoking user, so a root guest IS
# that user -- it opens /dev/kvm and owns the workspace it writes. A guest on
# 501 or 1000 lands on a subuid that owns nothing. It costs nothing on macOS,
# where hvi presents the host user's files as uid 0 by default.
#
# brig takes the guest account from the last element of this path.
guestHome: /root
forward: [GH_TOKEN]
# Codex signs in with `codex login --device-auth` and keeps the result in
# ~/.codex/auth.json, inside the persisted home. A forwarded OPENAI_API_KEY is
# the metered path and would take that decision away from you, so it is denied
# for the same reason ANTHROPIC_API_KEY is: put it in BRIG_FORWARD_ENV together
# with BRIG_ALLOW_DENIED=1 if metered billing is what you want.
deny: [OPENAI_API_KEY]
statePaths: [.codex]
headless: true
mem: 4096
cpus: 4cursor #
Cursor Agent -- example profile, image unpublished pending a terms check. Source: cursor.yaml.
name: cursor
desc: Cursor Agent -- example profile, image unpublished pending a terms check
binary: cursor-agent
# Built by community-images but deliberately never pushed, so this reference
# does not resolve. unpublished makes brig say that rather than surface a
# registry 404.
unpublished: true
image: ghcr.io/brig-sh/cursor:latest
# Leave network unset: Linux and hvi isolate; the default macOS backend, vz,
# falls back to shared because it cannot isolate networks.
guestHome: /home/cursor
forward: [CURSOR_API_KEY, GH_TOKEN]
statePaths: [.cursor, .local/share/cursor-agent]
headless: true
mem: 4096
cpus: 4gemini #
Gemini CLI (Google) -- example profile. Source: gemini.yaml.
name: gemini
desc: Gemini CLI (Google) -- example profile
binary: gemini
image: ghcr.io/brig-sh/gemini-stock:root
# Boots as an ordinary OCI image: the image carries no kernel, so the
# runtime supplies one. hvi is the backend that path is built around.
hypervisor: hvi
network: isolated
genericBoot: true
# The root account, which is what makes this work on a rootless Linux install:
# rootlesskit maps container uid 0 to the invoking user, so a root guest IS
# that user -- it opens /dev/kvm and owns the workspace it writes. A guest on
# 501 or 1000 lands on a subuid that owns nothing. It costs nothing on macOS,
# where hvi presents the host user's files as uid 0 by default.
#
# brig takes the guest account from the last element of this path.
guestHome: /root
forward: [GEMINI_API_KEY, GH_TOKEN]
statePaths: [.gemini]
headless: true
mem: 4096
cpus: 4grok #
Grok CLI (xAI) -- example profile. Source: grok.yaml.
name: grok
desc: Grok CLI (xAI) -- example profile
binary: grok
image: ghcr.io/brig-sh/grok-stock:root
# Boots as an ordinary OCI image: the image carries no kernel, so the
# runtime supplies one. hvi is the backend that path is built around.
hypervisor: hvi
network: isolated
genericBoot: true
# The root account, which is what makes this work on a rootless Linux install:
# rootlesskit maps container uid 0 to the invoking user, so a root guest IS
# that user -- it opens /dev/kvm and owns the workspace it writes. A guest on
# 501 or 1000 lands on a subuid that owns nothing. It costs nothing on macOS,
# where hvi presents the host user's files as uid 0 by default.
#
# brig takes the guest account from the last element of this path.
guestHome: /root
forward: [XAI_API_KEY, GH_TOKEN]
statePaths: [.grok]
headless: true
mem: 4096
cpus: 4opencode #
opencode (OSS, provider-agnostic) -- example profile. Source: opencode.yaml.
name: opencode
desc: opencode (OSS, provider-agnostic) -- example profile
binary: opencode
image: ghcr.io/brig-sh/opencode-stock:root
# Boots as an ordinary OCI image: the image carries no kernel, so the
# runtime supplies one. hvi is the backend that path is built around.
hypervisor: hvi
network: isolated
genericBoot: true
# The root account, which is what makes this work on a rootless Linux install:
# rootlesskit maps container uid 0 to the invoking user, so a root guest IS
# that user -- it opens /dev/kvm and owns the workspace it writes. A guest on
# 501 or 1000 lands on a subuid that owns nothing. It costs nothing on macOS,
# where hvi presents the host user's files as uid 0 by default.
#
# brig takes the guest account from the last element of this path.
guestHome: /root
forward: [OPENROUTER_API_KEY, GH_TOKEN]
statePaths: [.local/share/opencode, .config/opencode]
headless: true
mem: 4096
cpus: 4ubuntu #
A plain Ubuntu shell, running as root. Source: ubuntu.yaml.
name: ubuntu
desc: A plain Ubuntu shell, running as root
kind: shell
binary: bash
image: docker.io/library/ubuntu:latest
# Boots as an ordinary OCI image: the image carries no kernel, so the
# runtime supplies one. hvi is the backend that path is built around.
hypervisor: hvi
network: isolated
genericBoot: true
# The workspace is mounted at /root/work rather than over the home directory:
# this guest runs as root and the point of it is the machine, not an agent's
# state. guestHome names where the workspace lands, which is what every path
# calculation needs.
guestHome: /root/work
forward: [GH_TOKEN]
mem: 2048
cpus: 2