NOFire.ai
Blog/Engineering

Keep herdr, give each agent its own microVM

Brig now supports herdr. The herdr profile runs each coding agent in its own Brig microVM and lets one herdr client manage all of them over SSH, with no open port in the guest. A new herdr plugin, herdr-brig, turns the setup into one key: it boots a sandbox for a project, registers it as a herdr machine, and starts Claude Code in it. Another key removes it. This post covers what shipped, how to set it up, and its limitations.

Keep herdr, give each agent its own microVM

Brig now runs herdr. herdr keeps coding agents in workspaces and panes, on your machine or on remote ones, and lets you read and prompt any of them from one place. With Brig, each agent runs in its own microVM, and herdr manages each microVM as a remote machine.

This release has two parts:

  • The herdr profile in Brig, in Brig v0.5.2. Its image runs Claude Code with a herdr server, and herdr reaches it over SSH through brig exec. The guest has no open port. For the manual setup, see docs/herdr.md.
  • herdr-brig, a herdr plugin that automates that setup. One key creates a sandbox for a project, adds it to herdr, opens a workspace in the project, and starts Claude Code. Another key removes it.
ctrl+b m creates a Brig sandbox for ~/code/api and starts Claude Code in it, ctrl+b g jumps into it, ctrl+b v opens a shell in the guest, and ctrl+b M removes it. The boot wait plays at 2x.

Several agents on a laptop usually means several processes that run as you. Each one can read your home directory, your SSH keys, your cloud sessions, and every other checkout on the disk, and it can reach any host your laptop can. In a Brig sandbox, an agent reaches only the project you mount, its own guest home, and the credentials and hosts that its policy names.

We build Brig to put a microVM boundary around agent execution, and our engineering team runs its own agents in it.

Threat model

The threat model is the same as for a single agent. The agent reads untrusted input, such as issues, repository content, web pages, and log lines, and any of it can steer the agent into actions that nobody asked for. This setup assumes that happens.

  • Protected: the rest of your laptop, your credentials, and the other projects and agents next to this one.
  • Trusted: the host. The plugin runs on your machine with your herdr access. Brig keeps credentials away from the agent, not away from malware on your laptop.

How herdr reaches a sandbox

herdr manages a remote machine over SSH, but a Brig sandbox has no open port. Nothing in the guest listens for a connection.

Instead, the herdr profile uses the herdr-stock image, which contains sshd and a small wrapper, herdr-sshd. On the host, an SSH config block runs the wrapper through brig exec as its ProxyCommand, and the wrapper runs sshd on the exec's input and output:

Host herdr*+*
  User root
  ForwardAgent no
  StrictHostKeyChecking yes
  UserKnownHostsFile /dev/null
  KnownHostsCommand /bin/sh -c 'r=$(echo "$1" | sed "s/+/@/") && k=$(brig -q exec "$r" -- cat /etc/ssh/ssh_host_ed25519_key.pub 2>/dev/null) && [ -n "$k" ] && echo "$1 $k"' sh '%n'
  LogLevel ERROR
  ProxyCommand /bin/sh -c 'exec brig -q exec "${1%%%%+*}@${1#*+}" -- /usr/local/sbin/herdr-sshd 2>/dev/null' sh '%n'

A host name has the form <profile>+<sandbox>. For example, herdr+api is the sandbox herdr@api.

The block makes four choices:

  • It reads the host key through brig exec. The guest gets new host keys on every boot, so a known_hosts file passes only until the next boot. herdr checks host keys strictly, and an empty key fails the check.
  • It uses no SSH key. Anyone who can run brig exec on your host is already root in the guest, so a key guards nothing more.
  • It sets ForwardAgent no. A forwarded agent gives the guest a signer for every key you hold.
  • It quotes '%n' and writes %%%%. ssh puts the host name into a command line that your shell parses, so an unquoted name is glob expanded first and can select a different sandbox. ssh also turns %% into % before the shell runs, so %%%% is what reaches it as %%.

Each SSH connection is a new brig exec and carries a new session token for the broker routes. For details, see What is still exposed.

Before you begin

Make sure that you have the following:

  • macOS on Apple Silicon, or Linux with KVM.
  • Brig v0.5.2 or later, with the herdr profile. To check, run brig agent ls. To install Brig, see the guide to sandboxing your AI coding agent.
  • herdr 0.9 or later.

Install the plugin

  1. Install herdr-brig from its repository:

    herdr plugin install brig-sh/herdr-brig
  2. Verify that herdr lists the plugin's two actions, new and remove:

    herdr plugin action list --plugin brig-sh.herdr-brig
  3. herdr doesn't bind keys for a plugin. To bind the two actions, add the following to ~/.config/herdr/config.toml:

    [[keys.command]]
    key = "prefix+m"
    type = "plugin_action"
    command = "brig-sh.herdr-brig.new"
    description = "brig: new sandbox"
     
    [[keys.command]]
    key = "prefix+shift+m"
    type = "plugin_action"
    command = "brig-sh.herdr-brig.remove"
    description = "brig: remove sandbox"
  4. Check and reload the configuration:

    herdr config check
    herdr server reload-config

With the default prefix, ctrl+b m creates a sandbox and ctrl+b M removes one.

Give Claude Code a login

Claude Code in the sandbox needs a login. Choose one of the following options:

  • Import your login. Run brig secret import herdr. This command copies your Claude Code login from the keychain into Brig's secret store, and Brig mounts it into each run in memory. When the login expires, run the command again.
  • Keep the login on the host. Use a policy with a claude-code route. The guest then gets a session token for the route instead of the login. The policy in Limit GitHub access to one organization includes this route.

Create a sandbox

  1. Press ctrl+b m. herdr opens a popup:

    Project directory to mount, or - for none [/Users/you/code/api]:
    Sandbox name [api]:
     
    Policy for this sandbox:
      1) devwork
      -) none
    Choose 1 or - [-]:
    Start Claude Code in it now? [Y/n]
  2. On the first run, confirm that the plugin can add the SSH block from How herdr reaches a sandbox to ~/.ssh/config.

    The plugin backs up the file, inserts the block before the first Host or Match line, and runs ssh -G to verify that herdr+<name> resolves through the block. If the check fails, the plugin restores the backup.

  3. Enter the project directory. The plugin mounts it read-write at /work/<basename>.

    The default is the focused pane's directory, but only if that pane is local. A pane on a sandbox reports its guest's directory, so in that case the default is -, which mounts nothing.

  4. Enter a sandbox name, choose a policy, and confirm that you want to start Claude Code.

The plugin then runs the following commands, in order:

StepCommand
Bind the policybrig policy attach devwork herdr -n api, then brig policy check
Boot the sandboxbrig run -d herdr@api ~/code/api
Register the machineherdr machine add herdr+api --label api
Open a workspaceherdr --machine api workspace create --cwd /work/api
Start the agentherdr --machine api agent start api --kind claude

The policy applies to this sandbox only. If the policy defines a route with the same name as a route in a policy bound to the whole profile, brig policy check reports the clash, and the plugin stops before it creates anything.

The popup closes when the sandbox is ready. On an error, the popup stays open and shows the error. If herdr machine add fails, the plugin removes the sandbox that it booted.

Directories the plugin doesn't mount

The guest can write to anything that you mount. In some host directories, a written file becomes code or configuration that the host runs. The plugin refuses to mount these directories, whether you type them or the plugin suggests them:

  • /, your home directory, and any directory above it
  • every top-level dot directory in your home directory, such as ~/.ssh, ~/.config, and ~/.claude, and ~/Library
  • a directory on PATH, inside one, or containing one
  • /usr, /etc, /opt, /bin, /sbin, /System, /Library, /Applications, ~/bin, ~/go, and ~/Applications

Git checkouts

The agent works directly in your checkout, so its edits and commits appear in your working copy, and you review them where you already work.

A linked worktree or a submodule has a .git file that points into another repository's .git directory, which the guest can't see. The plugin mounts the files, but git fails in the sandbox with not a git repository. The popup warns you when this applies.

Limit GitHub access to one organization

An agent that works on a repository usually needs to push to it. To give the agent a GitHub token without putting the token in the guest, route it through Brig's credential broker and limit the route to one organization. For several organizations, create one policy per organization and choose the policy when you create the sandbox.

  1. Store the token as a Brig secret. Pipe it in so that it never appears in a command line:

    gh auth token | brig secret create gh-token

    To read the token from 1Password instead, run op read -n <ref> | brig secret create gh-token.

  2. Create a policy in ~/.config/brig/policies. The following example routes both the Claude Code login and the GitHub token through the broker, and denies all other egress:

    apiVersion: brig.sh/v1alpha1
    kind: Policy
    metadata:
      name: devwork
      description: Claude, and git over HTTPS to one GitHub org, through the broker
    spec:
      egress:
        default: deny
      routes:
        - name: anthropic
          preset: claude-code
          credential:
            secretRef: { name: claude-credentials }
        - name: github
          preset: github-token
          credential:
            secretRef: { name: gh-token }
          request:
            allow:
              repos: ["your-org/*"]
  3. List each spelling of the owner name that you use. The broker matches repos case-sensitively, but GitHub doesn't. For example, one of our policies lists both NOFireAI/* and nofireai/*.

With this policy, the guest gets the following, as described in Brig's presets:

  • A session token for each route, never the credential itself.
  • git configured to use the github route for https://github.com/ and [email protected]: URLs. The broker adds the token to each request.
  • A 403 response from the broker for any repository outside your-org/*.

The route covers git only. It doesn't cover gh or api.github.com.

We use one token per organization, and we verified that each token sees only its own organization's private repositories.

Remove a sandbox

  1. Press ctrl+b M. The plugin lists the herdr machines that are Brig sandboxes.
  2. Choose a sandbox and confirm.

The plugin disables the herdr machine, removes the sandbox and then the machine, and removes the sandbox's policy binding. Brig deletes the guest home. The mounted project stays on the host.

Drive the agents

Each sandbox is a herdr machine. To target it, pass --machine <label> to herdr's agent commands:

herdr --machine api agent list
herdr --machine api agent read w1:p1 --source recent --lines 40
herdr --machine api agent prompt api "Run the tests and summarize the failures" --wait

The plugin names each agent after its sandbox, so agent prompt api reaches the agent in api. With --wait, the command returns when the agent is idle, done, or blocked.

To copy files outside the project, use the same SSH host:

scp herdr+api:/root/out.log .

Known limitations

Enforced byWhat
BrigThe guest sees the mounted project and its guest home, and no other host files. It reaches only the hosts and routes that its policies name, and it holds session tokens, not credentials.
The brokerA route request comes from this sandbox, with a valid token for this route. A repository outside the allow list is refused.
The pluginThe mounted directory isn't one of the refused paths. A policy that clashes with the profile's policies is refused before anything boots.
NobodyThe agent's changes are what you wanted. Review them as you would any other commit.

Also note the following:

  • Unnamed hosts don't resolve. With egress: default: deny, ssh [email protected] fails with Could not resolve hostname github.com. The guest has no SSH key and no forwarded agent. Use git over HTTPS through a github-token route instead.
  • A broker restart ends sessions. A pane keeps the session token that it started with. After a broker restart, a running agent gets 401 … the session token was refused. To recover, start the agent again in a new pane.
  • An imported login expires. Run brig secret import herdr again, or keep the login on the host with a claude-code route.
  • A concurrent brig run can cut off a running sandbox. If a brig run -d fails with instance already exists, it removes the running sandbox from the broker, and the sandbox's exec token then returns 401. This is an open bug, brig-sh/brig#614.

We tested on macOS on Apple Silicon, with Brig built from main after v0.5.2 and herdr 0.9.5. The tests covered creating and removing sandboxes, with and without a mounted project, with and without a policy, the SSH block insertion, and the popup's error paths.

Open source

herdr-brig is open source under Apache 2.0, and so is Brig. Our enterprise offering around Brig adds host-side observation, centralized action records, and SIEM export for a fleet of agents. To see what's available, visit the enterprise agent security page, or talk to us about your deployment.

Open source

Don't trust the sandbox. Test it.

brig runs each coding agent in its own microVM, with its own Linux kernel, on your Mac or Linux machine. Apache 2.0, no account, no gated features. Every claim comes with the command that checks it.