Skip to content

About

Guest images for the coding agents brig runs, with open Dockerfiles

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

27 Commits

Folders and files

Repository files navigation

brig

community-images

Guest images for the coding agents brig runs, with open Dockerfiles.

brig boots each agent from an OCI image rather than running it on your machine. Those images have to come from somewhere, and baking them into the runtime would make them opaque -- you would be trusting a binary blob with your source tree and your provider keys. So they live here instead, one small Dockerfile per agent, in a public repo, built and published by a workflow you can read.

The point is that you can check what is in the sandbox before you put an agent in it. Read the Dockerfile, verify the signature on what we published, or ignore our images entirely and build your own. Any Linux CLI already works as a bring-your-own image; see docs/bring-your-own-image.md.

The images

Agent CLI Vendor Image
Claude Code claude Anthropic ghcr.io/brig-sh/claude-code
Claude Code (ubuntu account) claude Anthropic ghcr.io/brig-sh/claude-ubuntu
Codex codex OpenAI ghcr.io/brig-sh/codex
Gemini CLI gemini Google ghcr.io/brig-sh/gemini
Grok CLI grok xAI ghcr.io/brig-sh/grok
opencode opencode OSS ghcr.io/brig-sh/opencode
Cursor Agent cursor-agent Cursor not published, see below

Claude Code and Codex are the proven core. Gemini, Grok and opencode are example templates -- they build, and the CLI runs, but they have had less mileage than the first two.

Image names match the agent template names, so a template row and an image reference stay the same string.

Every image is credential-free by design. No key, no token and no session is baked into any of them. The agent authenticates at runtime and its state lands in the home directory, which is what brig persists for you.

What is in them

Ubuntu 24.04, the agent CLI, and enough to do real work with it:

base git, curl, wget, jq, and the usual networking tools
node 22 from NodeSource -- the agents' skills and plugins shell out to it
C/C++ build-essential, pkg-config
Python python3 with venv and pip
Rust rustup under /usr/local, with clippy and rustfmt
GitHub gh, from GitHub's signed apt repo

Rust is the expensive one -- it roughly triples the image. If that matters more to you than having it preinstalled, build your own without it: the Dockerfiles are small and the rustup stanza is one block to delete.

The agent user also has passwordless sudo, so the agent can install whatever a task turns out to need:

$ sudo apt-get update && sudo apt-get install -y libpq-dev

That is deliberate. The sandbox boundary is the VM, not the user account inside it, and an agent that cannot install a missing header is an agent that stops halfway through the job. Worth knowing if you run the stock variant somewhere the container boundary is your boundary, because it inherits the same sudoers rule.

Two variants of each

Each agent is published twice, from the same Dockerfile:

Image What it is
bootable ghcr.io/brig-sh/<agent> a microVM guest: guest kernel, urunit, urunc metadata. What brig boots.
stock ghcr.io/brig-sh/<agent>-stock an ordinary container image: the same rootfs, none of the microVM machinery.

The stock variant is for the cases where a microVM is not what you want -- running the agent under docker or podman, in a Kubernetes pod, in CI, or as a base to build your own guest image on. It is about 8MB smaller on the wire (codex 269MB against codex-stock 261MB): the guest kernel and the two init binaries, and not much else. The rootfs is the same, so most of the size is Ubuntu and the agent either way.

They are not two Dockerfiles. The #syntax line is stripped and the same file is built plainly, in the same CI job, on the same runner, against the same resolution of ubuntu:24.04. Two files, or even two jobs, could drift; this cannot. And make check-stock asserts the absence of /.boot/kernel, /urunc.json and /urunit rather than assuming it, because an image that quietly kept a 50MB kernel would still run fine under docker and nobody would notice.

docker run --rm -it ghcr.io/brig-sh/codex-stock codex --version

Cursor

images/cursor/ builds and is exercised on every pull request, but we do not publish it. Publishing would mean hosting a redistribution of Cursor's binary from our registry, and that is a question their terms have to answer first. That check is still open, so the image stays unpublished rather than published and quietly hoped about.

Nothing stops you building it yourself today. make -C images/cursor build fetches the CLI straight from Cursor and never touches our registry.

Pulling and verifying

docker pull ghcr.io/brig-sh/claude-code

Everything we publish is signed with keyless cosign. There is no key to distribute and no key for us to lose: the signature is bound to the workflow that produced the image, and it is recorded in Sigstore's public transparency log. To check a signature is really ours:

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

That command is the interesting one. It does not ask "was this signed?" -- it asks "was this built by that workflow, in that repo, on main?". A signature from anywhere else fails it.

The regexp is fussy on purpose, and both details are load-bearing. The dots are escaped, so build-images\.yml matches that file and not a file named something one character away from it. And it ends at refs/heads/main$, so a certificate minted from a branch or from a pull request does not satisfy it -- without that anchor the command would accept anything anyone could get to run in this repository, which is a much weaker claim than the one it looks like it is making. Copy it exactly.

Each image also carries an SPDX SBOM as a signed attestation, so you can see the packages inside before you boot it:

cosign verify-attestation --type spdxjson \
  --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 \
  | jq -r '.payload | @base64d | fromjson | .predicate.name'

Architectures

Every image is published for both arm64 and amd64: arm64 for brig on Apple Silicon, amd64 for urunc on Linux.

Tag What it is
:latest multi-arch index, resolves to the right one
:arm64, :amd64 the per-arch images
:arm64-<short-sha>, :amd64-<short-sha> immutable, for pinning a build
:uid1000 claude-code-stock only: the same image with the agent on uid 1000
:arm64-uid1000, :amd64-uid1000 the per-arch uid-1000 images
:root stock only: the same image ending on root in /root
:arm64-root, :amd64-root the per-arch root images

The default images put the agent on uid 501, the first human user on macOS, so that a file it writes to a shared directory comes back owned by you. On a Linux host that user is 1000, and :uid1000 is the same image built for it.

This is not a preference. hull resolves the image's configured user against the image's own /etc/passwd and hands the pair to the VMM as --fs-uid/ --fs-gid, which is what the shared home is presented as inside the guest. Run an image built for 501 where the host user is 1000 and every file arrives owned by somebody else; run one with no user configured at all and the share arrives owned by root, where the agent cannot create anything at the top of its own home and cannot repair it either -- chown on a virtiofs mount root returns EINVAL.

The uid tags exist on the stock package only. bunny ignores --build-arg, so the bootable variant cannot be built for a different uid; make check asserts the uid now rather than letting a mislabelled image be published.

claude-ubuntu is the way around that: it creates no user and runs as the ubuntu account the base already puts on 1000:1000, so it reads no build argument and both its variants are on 1000 by construction. Its images carry no uid tag because there is only one uid to have.

A rootless brig install wants the opposite. rootlesskit maps container uid 0 to the invoking user and everything above it to a subuid, so a guest on 501 or 1000 owns nothing on the host: it cannot open /dev/kvm and cannot write its own home. <agent>-stock:root is the same image ending on root, which puts the guest back on the invoking user and makes the files it writes that user's.

Every published agent has one. It is a multi-arch index like :latest, so a single profile works on either machine, and the per-arch -stock:<arch>-root tags stay for pinning. Use the plain image on a root install and the root tag on a rootless one.

A profile for a root image names the home it ends on, because brig derives the guest account from the last element of guestHome:

image: ghcr.io/brig-sh/claude-code-stock:root
guestHome: /root

Claude Code needs one thing more there. It refuses --dangerously-skip-permissions outright when it is running as root, so a profile for a root image sets IS_SANDBOX=1 to say the boundary is the VM rather than the account. The other CLIs have no such check.

The root tags are on the stock package only, for the same reason the uid tags are: bunny ignores --build-arg. That costs nothing today, because the profiles that boot these images are genericBoot profiles pointing at the stock package.

Prefer :latest or a digest. Pulling the wrong architecture here is worse than usual: each one bundles a different guest kernel, so it does not merely run slowly under emulation, it does not boot at all.

The difference is the setup each one boots in. arm64 runs under Apple Virtualization.framework with a virtiofs root; amd64 runs under urunc on Linux with an ext4 block root. Both currently boot on the kernel bunny bundles.

Guest kernels

A guest kernel is its own artifact, on its own cadence. An image rebuilds whenever an agent CLI moves, which is often; a kernel changes when the setup it boots in changes, which is rare. So Build kernel builds them separately and publishes them under ghcr.io/brig-sh/kernel, one tag per profile:

Profile Setup Root
arm64 brig on Apple Silicon, Virtualization.framework virtiofs
amd64 urunc on Linux ext4 block (devmapper snapshot)

Each publishes the bootable image, the exact config it was built with, and a config.diff against defconfig, so you can read what is in the kernel your agent runs on:

oras pull ghcr.io/brig-sh/kernel:amd64 -o ./k
grep -E 'VIRTIO_BLK|EXT4_FS|OVERLAY_FS' ./k/config

Every driver the guest needs to reach its root is compiled in. It boots with no initramfs, so such a driver cannot be a module -- it would live on the filesystem it is required to mount. Everything else stays modular and is never shipped, since the guest carries no /lib/modules.

kernel/build-kernel.sh hard-gates each required symbol to =y, plus the boot magic, that BTF was generated, and a size cap. Those gates earn their keep: scripts/config --enable is a request, not a guarantee, and the size cap already caught a config that built the whole defconfig driver set into a headless guest.

Wiring the images to boot these instead of bunny's default is the next step, and deliberately separate: the arm64-on-bunny-default configuration is the only one that has actually been booted.

Building locally

You need Docker and nothing else. The guest binaries are compiled inside containers, so there is no host toolchain to install -- this runs on an Apple Silicon Mac as-is.

make -C images/claude-code build check

build produces ghcr.io/brig-sh/claude-code:arm64 locally. check then asserts the result is actually bootable and that the agent runs as the image's unprivileged user. Useful knobs, all overridable on the command line:

Variable Default What it does
IMAGE ghcr.io/brig-sh/<agent>:<arch> Tag to build
PLATFORM linux/arm64 Build platform (linux/amd64 also builds the guest kernel)
AGENT_UID 501 uid of the in-guest user (1000 for a Linux host)
ALT_UIDS (empty) extra uids CI also publishes this image under
CLI_VERSION (empty) agent CLI release for an image that names its ARG in CLI_VERSION_ARG (the Claude images: CLAUDE_VERSION); empty keeps the Dockerfile's default
URUNC_SRC / URUNIT_SRC cloned into dist/src Guest init checkouts

AGENT_UID defaults to 501 because that is the first human user on macOS. Files the agent writes to a shared directory then carry your ownership and stay writable from both sides. On a Linux host, set it to your own uid.

How an image is put together

Two stages, and the split matters.

The Dockerfile is a bunny build. The #syntax line at the top swaps in bunny as the BuildKit frontend, which packages the result as a bootable microVM image rather than a plain container: a guest kernel under /.boot, the urunc metadata in /urunc.json, and the rootfs. Delete that one line and you get an ordinary OCI image, which is a perfectly good thing to have.

Dockerfile.overlay is then a plain docker build on top, copying in the guest init (urunit) and the exec agent (urunit-agent). It is separate on purpose: bunny injects its own stock urunit, and the agent-capable one has to override it. Both are compiled from source at build time, from public repos, so an image always ships the current init rather than whatever was vendored months ago.

images/agent.mk holds the actual build rules. A per-agent Makefile is four lines of data on top of it.

What is not here yet

Worth being explicit about, since these are the questions people ask.

No boot test in CI, on either architecture. make check proves an image has a kernel, its urunc metadata, both guest binaries and a runnable CLI. It does not prove the thing boots. arm64 would need Apple Virtualization.framework and therefore an Apple Silicon runner; amd64 would need a Linux runner with urunc and devmapper set up. Treat the published images as build-verified, not boot-verified.

Reproducibility is version-pinned, not bit-for-bit. Every agent CLI is pinned to an exact version in its Dockerfile, so a rebuild installs the same agent. The Ubuntu package set underneath still moves.

Adding an agent

Copy the closest existing directory and change the install stanza. That is the whole design -- the Dockerfiles are small and repetitive on purpose, so adding an agent is a copy and an edit rather than an exercise in build-system archaeology. See CONTRIBUTING.md.

License

Apache-2.0, see LICENSE. That covers the Dockerfiles and the build tooling in this repo. The agent CLIs the images install are each under their own vendor's terms.

About

Guest images for the coding agents brig runs, with open Dockerfiles

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages