Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

outrig is a command-line tool for running LLM agents against your repository with MCP tools isolated inside a podman-managed container that you describe with a Dockerfile. You stay in control of the environment those tools run in: which language toolchains are available, which MCP servers are wired up, what the agent’s view of the filesystem looks like.

The problem

Modern coding agents are good at running tool calls – reading files, editing them, executing shell commands, hitting external APIs. But “running tool calls” on a developer’s laptop means the agent can do anything the developer can: delete files outside the project, push to remote branches, talk to anything on the network. Most agent harnesses respond to this by gating each tool call behind a human-approval prompt. That works, but it defeats the point of letting the agent work – every prompt is a context switch back to the human.

outrig takes a different trade. Instead of approving each call, you set up a sandbox once: a Dockerfile, a config file, a network policy. The agent loop stays in the host outrig process, but the tool calls it can make go through MCP servers inside that sandbox. The blast radius of a wrong tool call is bounded by the container.

How it fits together

flowchart LR
    you(["you<br/>(terminal)"])
    api(["OpenAI-compatible<br/>HTTPS API"])
    repo[("your repo<br/>on disk")]

    subgraph host["host"]
        direction TB
        outrig["outrig CLI"]
        rig["Rig<br/>(agent loop)"]
        outrig --> rig
    end

    subgraph container["podman container - sleep infinity"]
        direction TB
        fs["fs MCP server"]
        shell["shell MCP server"]
    end

    you -- "prompts (stdin)" --> outrig
    rig -- "assistant text (stdout)" --> you
    rig <-- "chat + tool calls" --> api
    rig -- "tool call" --> fs
    rig -- "tool call" --> shell
    outrig -. "buildah build<br/>podman run" .-> container
    repo == "bind-mounted at /workspace" === container
  • outrig is a Rust CLI. You run it from a shell inside your repo.
  • Rig (rig-core) drives the agent loop: it talks to your configured LLM provider, decides when to call tools, feeds tool results back to the model.
  • podman is the container runtime. We use buildah to build the image from your Dockerfile (buildah and podman share image storage, so anything buildah builds, podman can run).
  • MCP servers (Model Context Protocol) are the actual tools – filesystem access, shell execution, whatever you install in your container. outrig speaks JSON-RPC to each one over podman exec’d stdio and exposes them to Rig as dynamic tools.

What outrig is not

  • Not a hosted service. It’s a CLI you run locally.
  • Not an LLM. You bring the model – any OpenAI-compatible endpoint, plus whatever providers Rig adds.
  • Not opinionated about what the agent can do. You write the Dockerfile, you pick the MCP servers. outrig is just the harness.
  • Not a replacement for human-in-the-loop review of outcomes. The agent has full filesystem access to the workspace you mount; you review what it did via git diff after the session ends, not via approval prompts during it.

What’s next

Quickstart

This walks you from a fresh repo to your first running agent in five minutes, mostly via outrig init (which orchestrates outrig config init and outrig image add under the hood).

Crate names matter: depend on outrig when embedding the library, and install outrig-cli when you want the command-line tool that provides the outrig binary. Both crates publish from this repository.

Prerequisites

  • Rust toolchain (you’ll need it to install outrig itself).
  • podman installed and working in rootless mode. Verify with podman info – it should print without errors.
  • buildah installed alongside podman. They share image storage by default.
  • An API key for an OpenAI-compatible chat endpoint, exported as OPENAI_API_KEY.
$ podman info | grep -E "rootless|graphRoot" | head -3
  graphRoot: /home/you/.local/share/containers/storage
  rootless: true

If podman info errors out, fix that before continuing – outrig delegates everything to it and won’t paper over a broken setup.

Install outrig

Install the CLI package to get the outrig command:

$ cargo install outrig-cli
$ outrig --version
outrig 0.1.0

Initialize the configs

In a fresh git repo, run outrig init:

$ mkdir -p hello-outrig && cd hello-outrig
$ git init && echo "hello, world" > HELLO.txt && git add . && git commit -m initial
$ outrig init

init walks you through the global config first (providers and models – skipped if it already exists), then the repo config (workspace, default agent), then offers to call image add for the first image-config. Every prompt shows its default in [default: ...]; press Enter to accept. Type ? and Enter at any prompt for an explanation and the available options.

If the built-in image prompts do not cover the environment you need, still start with outrig init for the provider, workspace, and agent scaffolding. Then use AI-assisted design to have an AI tool generate or refine the Dockerfile and [images.<name>] block. The MCP path gives the AI validators; outrig design prompt is the no-MCP fallback.

[outrig] no global config found at ~/.outrig/config.toml -- let's create one.
? Provider style [default: openai]:
? Provider name [default: openai]:
? Base URL [default: https://api.openai.com/v1]:
? API key environment variable [default: OPENAI_API_KEY]:
? Define a model now? [Y/n]:
? Model name [default: fast]:
? Model identifier [default: gpt-4o-mini]:
? Provider for this model [default: openai]:
? Use this model as default-model? [Y/n]:
[outrig] wrote ~/.outrig/config.toml

[outrig] no repo config at .agents/outrig/config.toml -- let's create one.

Configuring models
[outrig] models available in your global config: fast (default: fast)
? Would you like to configure LLM models specific to this repo? [Y/n]: n
? Set a default-model for this repo? [y/N]:

Configuring your first agent
? Agent name [default: coder]:
? Preamble (one line) [default: You are a careful coding assistant.]:

Configuring your first image
? Image-config name [default: hello-outrig-standard]:
? Workspace host-path [default: .]:
? Workspace container-path [default: /workspace]:

[outrig] wrote .agents/outrig/config.toml

? Base image [default: debian:bookworm-slim]:
? Language toolchains [default: ]: node
? MCP servers [default: fs]:
[outrig] wrote .agents/outrig/images/hello-outrig-standard/Dockerfile
[outrig] added [images.hello-outrig-standard] to .agents/outrig/config.toml

(In the transcript above we accepted most defaults by pressing Enter; only node was typed explicitly for toolchains.)

Two files were written under .agents/outrig/, plus your global config:

~/.outrig/config.toml
hello-outrig/
├── HELLO.txt
└── .agents/
    └── outrig/
        ├── config.toml
        └── images/
            └── hello-outrig-standard/
                └── Dockerfile

The generated MCP table starts with fs. The run transcript below also uses shell execution, so the example config includes a manual shell entry. Replace shell-mcp-command with the shell MCP server you install in the image.

Commit the agent config so it travels with the repo:

$ git add .agents/ && git commit -m "add outrig config"

The global file (with API keys etc.) is not in your repo – it’s per-user and references secrets via env-var substitution.

What got generated

~/.outrig/config.toml:

default-model = "fast"

[providers.openai]
style    = "openai"
base-url = "https://api.openai.com/v1"
api-key  = "${OPENAI_API_KEY}"

[models.fast]
provider   = "openai"
identifier = "gpt-4o-mini"

.agents/outrig/config.toml:

default-image = "hello-outrig-standard"
default-agent     = "coder"

[workspace]
host-path      = "."
container-path = "/workspace"

[agents.coder]
# model omitted -> falls back to default-model = "fast"
preamble = "You are a careful coding assistant."

[images.hello-outrig-standard]
dockerfile = ".agents/outrig/images/hello-outrig-standard/Dockerfile"
context    = ".agents/outrig/images/hello-outrig-standard"

  [images.hello-outrig-standard.mcp]
  fs    = { command = ["mcp-server-filesystem", "/workspace"] }
  shell = ["bash", "-lc", "exec shell-mcp-command"]

.agents/outrig/images/hello-outrig-standard/Dockerfile (excerpt):

FROM docker.io/library/debian:bookworm-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl git nodejs npm passwd \
 && rm -rf /var/lib/apt/lists/* \
 && npm install -g @modelcontextprotocol/server-filesystem

# Add the shell MCP package you chose, matching shell-mcp-command in config.toml.

WORKDIR /workspace
CMD ["sleep", "infinity"]

The Dockerfile is yours to edit – image add produces a known-good starting point, not a finished spec. Note there’s no USER directive and no useradd: outrig sets up a user matching your host UID/GID at run time, so the same image works for any host user without rebuilding. See Concepts -> Workspace.

Pre-build the image

The first outrig run would build the image automatically, but doing it explicitly is faster to verify and keeps the first run snappy:

$ outrig build
[outrig] image-config: hello-outrig-standard
[outrig] dockerfile:       .agents/outrig/images/hello-outrig-standard/Dockerfile
[outrig] context:          .agents/outrig/images/hello-outrig-standard
[outrig] image tag:        hello-outrig-standard:8c2a4f7e91d6b5a3
[buildah] STEP 1/N: FROM docker.io/library/debian:bookworm-slim
...
[outrig] image ready: hello-outrig-standard:8c2a4f7e91d6b5a3

A second outrig build is a cache hit:

$ outrig build
[outrig] image ready (cache hit: hello-outrig-standard:8c2a4f7e91d6b5a3)

Run an agent

$ OPENAI_API_KEY=sk-... outrig run

Diagnostics arrive on stderr:

[outrig] agent:             coder (model: fast / provider: openai / gpt-4o-mini)
[outrig] tool-call max:     50
[outrig] tool-result max:   262144 bytes
[outrig] image-config:  hello-outrig-standard
[outrig] image:             hello-outrig-standard:8c2a4f7e91d6b5a3 (cache hit)
[outrig] container started: outrig-20260502T103412-3f2a
[outrig] mcp fs:    initialized (3 tools)
[outrig] mcp shell: initialized (1 tool)
[outrig] tools available: fs__read_file, fs__list_directory, fs__write_file, shell__exec
[outrig] session id: 20260502T103412-3f2a   (Ctrl-D to exit, /help for slash commands)
> 

Anything you type at > is the user turn. Replies (only the assistant text) come back on stdout:

> what's in this repo and what does it do?
[outrig] tool call: fs__list_directory({"path": "/workspace"})
[outrig] tool call: fs__read_file({"path": "/workspace/HELLO.txt"})
This is a tiny git repo with one tracked file: HELLO.txt, which contains the
text "hello, world". There's no source code yet -- just the README-equivalent
and the outrig harness configuration under .agents/outrig/.
> 

Now ask it to do something useful:

> add a second line to HELLO.txt that says "from the agent",
> commit it on a new branch named agent/hello, and tell me
> what the new file looks like.
[outrig] tool call: shell__exec({"cmd": "git checkout -b agent/hello"})
[outrig] tool call: fs__write_file({"path": "/workspace/HELLO.txt",
                                    "contents": "hello, world\nfrom the agent\n"})
[outrig] tool call: shell__exec({"cmd": "git add HELLO.txt && git commit -m 'append agent line'"})
[outrig] tool call: fs__read_file({"path": "/workspace/HELLO.txt"})
Done. I created branch agent/hello, appended "from the agent" to HELLO.txt,
and committed the change. The file now reads:

    hello, world
    from the agent
> 

Press Ctrl-D to end the session. outrig stops the container, finalizes the session record, and exits.

[outrig] session 20260502T103412-3f2a ended (duration 2m18s, exit 0)
$

Review what happened

The agent wrote directly to your repo (we used a direct bind-mount, not a staging copy – see Concepts -> Workspace). Use git to inspect:

$ git status
On branch agent/hello
nothing to commit, working tree clean

$ git log --oneline -2
b41af3a append agent line
12c0a99 add outrig config

$ outrig ls
ID                       STARTED              DURATION  CONTAINER              EXIT
20260502T103412-3f2a    2026-05-02 10:34:12  2m18s     hello-outrig-standard  0

If you don’t like what the agent did, the rollback is whatever you’d normally do with git: git checkout main, git branch -D agent/hello, etc. outrig itself doesn’t try to take that work back from you.

Where to go next

Concepts

This section explains the moving parts an outrig user actually has to think about. If you’ve worked through the Quickstart, you’ve already touched all four – this is where they’re spelled out.

  • Containers – the Dockerfile that defines the agent’s environment, the [images.<name>] config block, named image-configs for switching between toolsets, and the UID/GID convention that keeps file ownership sane.
  • MCP Servers – how outrig discovers and routes tool calls, the <server>__<tool> name prefix, lifecycle and crash behavior, stderr capture.
  • MCP Trust Model – why the container is the MCP trust boundary and why tools can be configured liberally inside it.
  • Workspace – what the agent can reach on your filesystem, the direct bind-mount model, why outrig doesn’t stage changes, how to review with git.
  • Providers, Models, and Agents – the three-layer LLM config: providers (where), models (what), agents (which preamble + image). Most users keep providers and models in the global config; agents are typically per-repo.
  • In-process LLMs – a feature-gated provider that runs the model inside the outrig process itself, for questions whose content must not leave the host. Plumbing for the future egress filter, tool-use filter, and prompt-injection scanner.

Containers

The container is the agent’s whole world. It’s where MCP servers run, where shell commands execute, where files get read and written. You define it with a Dockerfile you commit to your repo, and outrig builds it with buildah and runs it with podman.

Default location

outrig image add writes image files under .agents/outrig/images/<name>/:

.agents/outrig/
├── config.toml
└── images/
    └── coding/
        ├── Dockerfile
        └── (any other files referenced by the Dockerfile)

Putting them under .agents/outrig/ keeps outrig-specific build context separate from your project’s own Dockerfiles (which often live at the repo root or under containers/, docker/, etc. for unrelated purposes). You can override the default by editing [images.<name>].dockerfile and .context to point anywhere relative to the repo root.

Dockerfile conventions

outrig expects a few things from your Dockerfile.

CMD ["sleep", "infinity"]

The container’s job is to stay running while the agent works. Every real process – MCP servers, shell commands the agent runs – is launched via podman exec from outrig on the host. So your Dockerfile ends with:

CMD ["sleep", "infinity"]

If you set a different CMD or ENTRYPOINT, the container will exit before outrig can attach an MCP server to it. outrig doesn’t override your CMD; it relies on this convention.

Don’t set up a user in the Dockerfile

outrig handles user identity entirely at run time – see Workspace. Don’t useradd a hard-coded UID in your Dockerfile, and don’t set a USER directive. The build is intentionally generic so the same image works for any host user (yours, a teammate’s, CI’s) without rebuilding.

The image needs useradd and groupadd available (the standard passwd/shadow package on Debian/Ubuntu, shadow on Alpine). outrig calls them once at start-up to materialize an in-container user matching your host UID/GID.

Install MCP servers

The image needs to contain the binaries and dependencies for every MCP server you reference in the matching [images.<name>.mcp] config. There’s no other place to put them – outrig doesn’t fetch tools at run time.

RUN npm install -g @modelcontextprotocol/server-filesystem

If multiple MCP servers need different language toolchains (one needs Node, one needs Python), install both in the same image. The agent’s whole MCP set runs in one container per session.

The [images.<name>] config block

A typical config has at least one [images.<name>] block plus a top-level default-image:

default-image = "coding"

[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"   # relative to repo root
context    = ".agents/outrig/images/coding"              # relative to repo root
build-args = { NODE_VERSION = "20" }                          # extra Dockerfile ARGs

  [images.coding.mcp]
  fs    = { command = ["mcp-server-filesystem", "/workspace"] }
  shell = ["bash", "-lc", "exec shell-mcp-command"]

dockerfile and context are paths from the repo root (the directory containing .agents/outrig/). build-args are extra Dockerfile ARGs – whatever your Dockerfile needs parameterized at build time.

The [images.<name>.mcp] map is covered in MCP Servers.

Capability profiles

By default, outrig preserves podman’s default Linux capability set. That keeps existing toolchains and MCP servers working while still applying --security-opt=no-new-privileges. When a container can run with less privilege, add a security block:

[images.coding.security]
capability-profile = "no-net-raw"

The supported profiles are:

  • default: keep podman’s default capability set.
  • no-net-raw: drop NET_RAW, which blocks raw sockets without breaking most development tooling.
  • drop-all: start from --cap-drop=ALL.

You can combine a profile with explicit overrides:

[images.web.security]
capability-profile = "drop-all"
cap-add = ["NET_BIND_SERVICE"]

Explicit cap-add values are rendered last, so a container can start from drop-all and add back one narrow capability. Capability names may include or omit the CAP_ prefix.

Using a pre-built image

If you already have an image (from a registry, CI pipeline, or local build), set image-name instead of dockerfile + context:

[images.scratch]
image-name = "docker.io/library/ubuntu:24.04"

  [images.scratch.mcp]
  fs = { command = ["mcp-server-filesystem", "/workspace"] }

Exactly one of these two shapes must be set on each block:

  • dockerfile + context (with optional build-args) – build path.
  • image-name – use-existing-image path.

Setting both, neither, or image-name alongside build-args is a config-validation error.

outrig build --image scratch pulls the image if not already local:

$ outrig build --image scratch
[outrig] image-config: scratch
[outrig] image:            docker.io/library/ubuntu:24.04
[outrig] image ready: docker.io/library/ubuntu:24.04

On subsequent runs when the image is already present:

$ outrig build --image scratch
[outrig] image ready (already pulled: docker.io/library/ubuntu:24.04)

outrig run --image scratch starts the container directly – no buildah invocation.

Named image-configs

You can declare multiple image-configs for the same repo and switch between them with --image:

default-image = "coding"

[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"
context    = ".agents/outrig/images/coding"

  [images.coding.mcp]
  fs    = { command = ["mcp-server-filesystem", "/workspace"] }
  shell = ["bash", "-lc", "exec shell-mcp-command"]

[images.planning]
dockerfile = ".agents/outrig/images/planning/Dockerfile"
context    = ".agents/outrig/images/planning"

  [images.planning.mcp]
  fs       = { command = ["mcp-server-filesystem", "/workspace"] }
  research = { command = ["mcp-research-tools"] }
$ outrig run                       # uses default-image = "coding"
$ outrig run --image planning      # different Dockerfile, different MCPs

This is useful when you want lighter-weight environments for different kinds of work – e.g. a planning image that has no compiler, no shell, and only research-oriented MCPs; a coding image with the full toolchain. Agents can also pin their own default image via agents.<name>.image; see Providers, Models, and Agents.

Every image-config is built and cached independently. Switching between them is fast after the first build.

Image caching

outrig tags built images as <image-config-name>:<hash>, where the name is the [images.<name>] block key and the hash is a content-addressed cache key combining the contents of the Dockerfile, the build-args, the OutRig labels derived from [images.<name>.mcp], and the content of the build context (gitignore-aware when the context is in a git repo, otherwise a tarball hash). So [images.outrig-standard] builds to outrig-standard:<hash>, which podman shows as localhost/outrig-standard.

Repo-local build images carry an org.outrig.mcp label too. On a cache miss, outrig builds a temporary image, reads any inherited/Dockerfile MCP label, overlays [images.<name>.mcp], and commits the final cache tag with the merged label. This keeps outrig image inspect <image-config-name>:<hash> aligned with the declared servers that startup will use.

Because the name is the image’s repository, give image-configs repo-specific, lowercase names (e.g. outrig-standard, not standard) so podman images makes clear which repo an image came from. Build-image names must be valid container image repository components – lowercase alphanumeric separated by ., _, or - – and outrig rejects invalid names at config load.

A change to the Dockerfile, any file in the context, build args, or [images.<name>.mcp] causes a rebuild on the next outrig run or outrig build. Otherwise the cache hit is immediate. To force a rebuild without changing files, run outrig build --no-cache.

Image-name configs use podman’s local image store directly; there is no <name>:<hash> tag in that path. --no-cache on an image-name config re-runs podman pull even when the image is already present locally. (The library Outrig::launch API, which builds from a raw Dockerfile with no image-config name, falls back to the outrig-cache:<hash> repository.)

What outrig sets in the run

outrig adds --userns=keep-id, --security-opt=no-new-privileges, the primary workspace bind-mount, any configured extra workspace mounts, and the runtime user-mapping bootstrap (see Workspace). Capability flags are emitted only when the selected image-config opts into a capability profile or explicit cap-drop / cap-add entries.

outrig does not configure seccomp profiles, AppArmor policy, SELinux policy, read-only root filesystems, or network egress policy in this container launch path. Network audit/filter mode is a separate session-level interceptor; see Workspace.

See also

MCP Servers

MCP – Model Context Protocol – is the wire format outrig uses to talk to tools. Each MCP server is a child process that runs inside your container and speaks JSON-RPC over its stdio. outrig connects to each one by podman exec -i’ing into the container, hands the resulting stdio pair to the rmcp client, and treats every tool the server advertises as a Rig dynamic tool.

The same [images.<name>.mcp] table is consumed by both outrig run and outrig mcp. outrig run registers those tools with its built-in agent; outrig mcp republishes them as one stdio MCP server for an external client. See Usage -> outrig mcp for client setup and transport rules. When outrig mcp --attach points at an existing container, it still starts its own MCP child processes inside that container; it does not share or proxy the host session’s existing MCP protocol state.

Declaring servers

MCP servers are configured per-image, as a map keyed by the server’s local name:

[images.coding.mcp]
fs    = { command = ["mcp-server-filesystem", "/workspace"] }
shell = ["bash", "-lc", "exec shell-mcp-command"]
build = { command = ["cargo-mcp"], env = { CARGO_HOME = "/workspace/.cargo" } }

shell-mcp-command is a placeholder for the shell MCP package you choose and install in the image. OutRig supports arbitrary MCP commands; outrig image add only renders package recipes for the MCP servers it can install without more input.

Each entry is one of:

  • A bare array (short form): ["bin", "arg1", ...]. Equivalent to { command = ["bin", "arg1", ...] } with no extra env.
  • A table (full form): { command = [...], env = { KEY = "value", ... } }. The env map is added to the podman exec invocation for this server.

Server names must match ^[a-zA-Z][a-zA-Z0-9_-]*$ and must be unique within an image-config. Names are how you reference servers elsewhere – in outrig logs <session> <server>, in tool-call traces, in the prefix that gets attached to every tool the server exposes.

Embedding MCP config in the image

An image can also carry its MCP declarations in its org.outrig.mcp OCI label. You author them as a [mcp] table in the project’s image.toml, which outrig image build serializes into the label:

# image.toml
[mcp]
fs    = { command = ["mcp-server-filesystem", "/workspace"] }
shell = ["bash", "-lc", "exec shell-mcp-command"]
build = { command = ["cargo-mcp"], env = { CARGO_HOME = "/workspace/.cargo" } }

The [mcp] table uses the same short and full entry shapes as [images.<name>.mcp]. At session startup, outrig reads the org.outrig.mcp label off the image, then overlays entries from config.toml. If both sources define the same server name, the config.toml entry replaces the image entry in full; fields are not deep-merged. Servers that appear in only one source remain in the merged set.

For build-from-Dockerfile repo images, outrig build also stamps the cache image with the same merged org.outrig.mcp label it would use at startup. The startup overlay still runs, but is idempotent for those repo-local entries. This means outrig image inspect <name>:<hash> can show the declared servers without starting a container.

Use embedded MCP config when a shared image owns the tool binaries and their default commands. Use config.toml for repo-local additions or overrides. A repo that wants to delegate completely to the image can omit [images.<name>.mcp].

The org.outrig.mcp label is not required; this allows a shared image to be used with different configurations via config.toml. Malformed JSON, invalid server names, and empty command arrays are startup errors because they mean the image metadata is broken.

To inspect what will actually start, run:

outrig mcp show-merged --image coding

The command starts the selected container, reads the org.outrig.mcp label, applies config.toml overrides, prints the effective [mcp] table to stdout, and then stops the container.

Lifecycle

When outrig run starts, the sequence per MCP server is:

  1. podman exec -i <container> <command> – outrig spawns the server as a child process inside the running container, with stdin/stdout piped back to outrig.
  2. Initialize handshake – outrig sends the MCP initialize request and reads the server’s capabilities.
  3. Discover tools – outrig calls tools/list and receives the list of advertised tools, each with a name, description, and JSON Schema for its inputs.
  4. Register with Rig – each discovered tool becomes a McpToolAdapter that implements Rig’s dynamic-tool trait. The agent now has access to it.

All servers come up before the REPL accepts any input. If any server fails to initialize, outrig run reports the error on stderr and exits before the REPL starts – you don’t get partial sandboxes.

When the REPL terminates (Ctrl-D, Ctrl-C, or LLM error), outrig closes each server’s stdin in turn, waits up to 5 seconds for the process to exit, then podman stop’s the container. Servers don’t see SIGTERM directly; they see EOF on stdin, which the MCP spec defines as the normal shutdown signal.

In attach mode, outrig mcp --attach borrows a container that something else owns. It shuts down only the MCP children it started and leaves the borrowed container running. If the owner stops the container while the attacher is live, the attacher exits instead of trying to relaunch it.

Tool name prefixing

Two different MCP servers can have a tool with the same name (e.g. both fs and archive might define read_file). To avoid collisions and keep the LLM’s tool-name space predictable, outrig always prefixes every tool with its server name:

fs__list_directory
fs__read_file
fs__write_file
shell__exec
build__cargo_check

The separator is __ (double underscore). The combined name is sanitized to fit OpenAI’s ^[a-zA-Z0-9_-]{1,64}$ constraint – non-matching characters become _, and over-long names get truncated with a stable hash suffix.

The LLM sees fs__write_file in its tool list and emits tool calls under that name. outrig’s router strips the prefix and dispatches to the correct MCP client with the original tool name.

You can list every tool currently registered with the agent from inside the REPL:

> /tools
[outrig] tools available (4):
  fs__list_directory   List the contents of a directory.
  fs__read_file        Read the contents of a file.
  fs__write_file       Write contents to a file (overwrites).
  shell__exec          Run a shell command and return its stdout/stderr.
>

What if a server crashes mid-session?

A crashed MCP server is surfaced as a tool-call error to the LLM, which usually causes the model to stop calling that tool and tell you about the failure. outrig does not auto-restart MCP servers in v0 – the server is gone for the rest of the session.

TODO: Incomplete – auto-restart and per-server health-checking are deferred.

The server’s stderr, captured to <session_dir>/logs/<server>.stderr, usually has the actual error. See Sessions for how to view it.

Attach mode can run more than one copy of the same MCP server in one container. Prefer servers that are reentrant-safe: no fixed listening port, global pidfile, or exclusive lock unless the server is explicitly designed to coordinate multiple copies. If a server cannot run twice, the second copy should fail clearly during startup and its stderr log will show the underlying conflict.

Picking which servers to include

Two practical guidelines:

  • Match servers to the image-config’s purpose. A planning image probably doesn’t need shell. A coding image almost certainly needs both fs and shell.
  • Fewer servers is better when it’s enough. Every server is another initialize cost at startup, another tool list cluttering the LLM’s prompt, another process to monitor. If a single MCP server covers your needs, use one.

See also

  • Containers – the Dockerfile that has to install the server binaries.
  • MCP Trust Model – the container boundary that makes broad MCP tools practical.
  • AI-assisted design – use outrig mcp self to design custom MCP-enabled image-configs.
  • Sessionsoutrig logs <session> <server> for stderr.
  • Reference -> Config – full schema for the [images.<name>.mcp] block.

MCP Trust Model

OutRig treats the container as the trust boundary for MCP servers. The tools run inside the container, see the container filesystem, and execute with the user mapping OutRig creates at session startup. The host stays outside that boundary except for the workspace mount and the runtime services OutRig explicitly connects.

That means an MCP server inside the container does not need a second application-level sandbox just to be safe for normal agent work. The container is already the place where the agent is allowed to inspect files, run commands, and coordinate tools.

Filesystem tools

Filesystem MCP servers can be pointed at broad container paths, including /, when that is the right shape for the container. “The agent can read everything” means everything in the container, not everything on the host.

For a coding container, /workspace is usually the right default because it matches the mounted repo. For a purpose-built operations container, a broader root can be useful if the image includes reference files, generated config, local SDKs, or test fixtures outside the workspace.

Shell tools

Shell MCP servers do not need command allowlists inside a normal OutRig container. Arbitrary code execution inside the container is the point of giving an agent a development environment. If a tool needs bash, package managers, compilers, or project-specific CLIs, install them in the image and let the MCP server expose them.

Keep secrets and host-only material out of the container unless the agent should be able to use them. Environment variables passed to MCP servers are part of the container tool boundary.

Network tools

Network-capable MCP servers are bounded by the container network namespace and by the host network policy around the container runtime. Future network interception can add host-level allowlists, but MCP server config should still describe what the tool needs to do rather than pretending the server is harmless.

Practical posture

Configure MCPs liberally inside the container and keep the container boundary deliberate. Install the tools the agent needs, expose useful paths, and rely on OutRig’s host/container split instead of building a narrow MCP config that prevents useful work.

See also

  • Containers – Dockerfile conventions and runtime user mapping.
  • MCP Servers – declaring the tools that run inside the container.
  • AI-assisted design – using outrig mcp self to design image-configs with an external AI tool.

Workspace

The workspace is what the agent’s tools get to read and write. By default, your repository is mounted live into the container at /workspace. You can also mount extra resource directories, read-only by default, when the agent needs supporting material outside the repo. Read this page before you run outrig on anything you can’t easily roll back.

Direct bind-mount, no staging

outrig mounts the primary host workspace directly:

podman run -v <repo>:/workspace:rw --userns=keep-id ...

That means changes made by tools inside the container appear on your host filesystem immediately for any read-write bind mount. There is no staging directory, no overlay, no per-session shadow copy. If the agent runs rm -rf /workspace/*, your repo is gone – recoverable only via git or whatever backup you have. Extra mounts default to read-only, but an extra access = "read-write" mount has the same immediate-write behavior.

This is intentional. The alternative – staging changes in a sandbox and asking you to “apply” them after the session – has a few real costs:

  • Upfront copy time, every session, on potentially large repos.
  • A second source of truth between session-end and apply-time that can drift.
  • Friction every time you want to actually use the agent’s output.
  • An apply step that may merge surprisingly with files you’ve edited concurrently.

For an autonomous-agent workflow, the better safety net is git, not a staging step:

  • Run outrig on a feature branch.
  • Commit (or stash) your work-in-progress before starting.
  • After the session, use git diff, git status, git checkout -- <path>, git reset --hard, etc. to review and roll back as you would any change.
  • If the agent really destroyed something, that’s what git reflog is for.

If you find yourself wanting a “review before apply” step, that’s a signal you don’t yet trust the agent’s environment – usually the right fix is to tighten the Dockerfile or the MCP server choices, not to layer a staging mechanism over the top.

UID/GID: runtime user mapping

By default, rootless podman maps the container’s UID 0 (root) to your host UID. Anything the container does as root therefore appears on the host as files you own – but anything done as a non-root in-container UID maps to some scrambled subuid number that’s awkward to clean up.

outrig sidesteps this in two parts:

  1. --userns=keep-id on the run command. With this, your host UID/GID map straight onto the same UID/GID inside the container’s user namespace – so an in-container process running as UID 1000 produces files owned by host UID 1000 on the bind-mount.
  2. A startup bootstrap that creates a matching user inside the container at run time, so tools that call getpwuid() (some shells, npm postinstall scripts, etc.) don’t fail on a missing /etc/passwd entry.

After podman run -d brings the container up, but before any MCP server starts, outrig (running as in-container root) does:

# Group: reuse if a group with this GID already exists, else create one.
gid=$(id -g) ; gname=$(id -gn)
existing=$(getent group "$gid" | cut -d: -f1)
if [ -z "$existing" ]; then
    # name collisions: append _ until groupadd succeeds
    until groupadd --gid "$gid" "$gname"; do gname="${gname}_" ; done
fi

# User: same dance.
uid=$(id -u) ; uname=$(id -un)
existing=$(getent passwd "$uid" | cut -d: -f1)
if [ -z "$existing" ]; then
    until useradd -u "$uid" -g "$gid" "$uname"; do uname="${uname}_" ; done
fi

# Some tools assume $HOME exists.
mkdir -p "/home/$uname"
chown "$uname:$gname" "/home/$uname"

After bootstrap, every podman exec outrig issues – to start MCP servers, to run anything else – uses --user=$(id -u):$(id -g). Files written under /workspace therefore appear with your UID/GID on the host. The image itself doesn’t need any user setup – whatever base image you pick works as long as useradd/groupadd are available.

The collision dance handles the case where the image already has a group or user at your UID/GID (common for 1000:1000 – the typical first non-root user in many distros). When that happens, outrig reuses the existing entry rather than creating a duplicate.

What’s mounted, what isn’t

The [workspace] block controls what host directories the container sees:

[workspace]
host-path      = "."          # relative to the repo root containing .agents/outrig/
container-path = "/workspace"

[[workspace.mounts]]
host-path      = "../shared-docs"
container-path = "/resources/shared-docs"

[[workspace.mounts]]
host-path      = "/var/tmp/outrig-cache"
container-path = "/resources/cache"
access         = "read-write"

host-path = "." (the default) mounts your whole repo. You can narrow this – e.g. host-path = "src" mounts only the source dir. The primary workspace is always read-write and becomes the container workdir.

Extra workspace.mounts entries are for supporting directories: sibling repos, generated docs, SDK checkouts, model artifacts, or caches. Relative extra host paths resolve against the repo root, just like the primary workspace. Their container paths must be absolute, cannot be /, and must not duplicate the primary workspace or another extra mount. Exact duplicates fail during config validation instead of relying on podman mount ordering.

Extra mounts default to access = "read-only". Use access = "read-write" only when the agent really should mutate that host directory, such as a scratch cache under /var/tmp.

Files outside the declared bind-mounts are not reachable from the container under any circumstances:

  • ~/.ssh – not mounted, agent can’t read your keys.
  • ~/.config – not mounted.
  • /etc/passwd on the host – not mounted; the container has its own.

The container has its own /etc, /home, /tmp, etc. coming from the image. The only windows onto the host filesystem are the primary workspace and the extra mounts you declare.

Network is not part of the workspace

By default, outrig grants the container full outbound network access. This means an agent with shell access can run curl, git push, npm publish, etc. – anything that talks to the outside world.

Network audit and filter modes are opt-in. Set one in config, or override one fresh session with --network audit or --network filter, to put a host-side interceptor in the path of outbound container traffic:

[network]
mode = "audit"

Audit mode writes one Zeek conn.log-style JSON object per connection to <session_dir>/logs/network.jsonl. Audit mode is allow-and-log only: every connection is still allowed, but records include the best known host, destination IP and port, transport, service, byte counts, and duration. HTTPS remains opaque except for TLS SNI. URL, method, status, and body inspection are deferred.

Filter mode uses the same interceptor and audit log, then applies global host/port policy before opening upstream TCP connections:

[network]
mode    = "filter"
default = "deny"
allow   = ["github.com:443", "*.npmjs.org"]
deny    = ["*:22"]

Policy is global-only because it belongs to the machine running the agent. Repo config may choose network.mode, but cannot set default, allow, or deny. Deny entries win over allow entries, and unmatched connections use default. A denied connection is closed immediately and still writes a network.jsonl record with outrig.action = "deny" and zero byte counts, so the audit log is the place to diagnose network policy failures.

When network mode is default, outrig leaves Podman’s configured default networking in place, does not install nftables rules, does not rewrite container DNS, and does not create network.jsonl. Systems without nftables or namespace support therefore keep running normal sessions. If audit or filter mode is explicitly enabled and the host cannot install the interceptor, startup fails before MCP servers launch.

See also

  • Containers – Dockerfile conventions, including the UID/GID setup.
  • MCP Servers – the layer that bounds what the agent can actually do with the workspace.
  • Reference -> Config – full schema for the [workspace] block.

Providers, Models, and Agents

outrig delegates LLM calls to the Rig crate, but configures the LLM stack in three layers so the same providers and models can be reused across many repos without copy-pasting:

  • Provider – where to talk to (base-url, api-key, wire format).
  • Model – a named identifier living on one provider (e.g. gpt-4o-mini on openai).
  • Agent – a runnable unit: model + system preamble (+ optional default image).

Most users keep providers and models in the global config (~/.outrig/config.toml) since those depend on the user’s accounts and preferences. Agents typically live in the repo config because their preambles and image choices are project-specific.

flowchart LR
    user[("user / API key")]
    subgraph global["~/.outrig/config.toml"]
        prov["[providers.openai]<br/>base-url<br/>api-key"]
        m1["[models.fast]<br/>identifier=gpt-4o-mini"]
        m2["[models.smart]<br/>identifier=gpt-4o"]
    end
    subgraph repo[".agents/outrig/config.toml"]
        a1["[agents.coding]<br/>preamble"]
        a2["[agents.review]<br/>preamble"]
        cont["[images.coding]"]
    end
    user --> prov
    m1 --> prov
    m2 --> prov
    a1 --> m1
    a2 --> m2
    a1 -. "image" .-> cont

[providers.<name>]

A provider is a wire-format + endpoint + API key.

[providers.openai]
style    = "openai"
base-url = "https://api.openai.com/v1"
api-key  = "${OPENAI_API_KEY}"

style is the protocol. v0 wires "openai" for any OpenAI-compatible endpoint, and recognizes "mistralrs" for in-process LLMs (gated behind a Cargo feature – see In-process providers below). base-url is the HTTPS endpoint. api-key must be the ${ENV_VAR} form – outrig resolves it at run time, never reads a key from disk. See Reference -> Config for the exact rules.

You can declare as many providers as you want – one per account, one per local Ollama install, one per OpenAI-compatible aggregator. Names you pick (e.g. openai, local-ollama, work-account) become labels you reference from models.

[models.<name>]

A model picks a specific identifier on a specific provider.

[models.fast]
provider   = "openai"
identifier = "gpt-4o-mini"

[models.smart]
provider   = "openai"
identifier = "gpt-4o"

provider references one of the names you defined under [providers.<name>]. identifier is whatever string the provider expects in its API request’s model field.

The model layer exists so that agents can refer to a stable name (fast, smart) and swap the underlying API model without touching every agent. If OpenAI renames a model, you edit one identifier; every agent using that name picks up the change.

[agents.<name>]

An agent ties a model to a system preamble and (optionally) a default image.

[agents.coding]
# model omitted -> falls back to top-level default-model
image       = "coding"
preamble    = "You are a careful coding assistant. Repo is at /workspace."
temperature = 0.2
tool-call-max = 300
tool-result-max = 1048576

[agents.review]
model    = "smart"      # explicit override of default-model
preamble = "You are a meticulous code reviewer. Be specific about line numbers."

model is optional. If set, it must reference one of the names you defined under [models.<name>]; if omitted, the agent inherits the top-level default-model (typically declared in ~/.outrig/config.toml). image is optional too: if set, outrig run --agent <name> defaults to that image-config. preamble is the system prompt the agent operates under – the place to encode role, scope, voice.

temperature and max-tokens live on the agent because the same underlying model is often used with different sampling for different tasks (e.g. low temperature for code, higher for brainstorming).

tool-call-max also lives on the agent when a role needs longer tool loops. If unset, the agent uses the top-level tool-call-max, then the compiled-in default of 50. The max is per user turn, so typing a follow-up prompt starts a fresh count while keeping conversation history. tool-result-max works the same way for oversized MCP output: an agent can inherit the top-level max or set its own byte max when a role regularly reads larger files or logs.

default-model at the top level

Most users have one preferred model and reuse it across repos. Set it once globally:

# ~/.outrig/config.toml
default-model = "fast"

Now any agent that omits model picks it up automatically. Per-repo overrides still work – write default-model = "smart" at the top of a repo config and that repo’s agents fall back to "smart" instead. Per-agent overrides still work too: agents.<a>.model = "smart" wins over both defaults.

Pointing at OpenAI-compatible endpoints

Anything that speaks the OpenAI Chat Completions wire format works as a style = "openai" provider:

[providers.together]
style    = "openai"
base-url = "https://api.together.xyz/v1"
api-key  = "${TOGETHER_API_KEY}"

[providers.openrouter]
style    = "openai"
base-url = "https://openrouter.ai/api/v1"
api-key  = "${OPENROUTER_API_KEY}"

[providers.local-ollama]
style    = "openai"
base-url = "http://localhost:11434/v1"
api-key  = "${OLLAMA_API_KEY}"   # set to anything; some servers ignore the header
[models.tg-llama]
provider   = "together"
identifier = "meta-llama/Llama-3.3-70B-Instruct-Turbo"

[models.or-claude]
provider   = "openrouter"
identifier = "anthropic/claude-sonnet-4-6"

The agent loop is unchanged – it’s still tool calls in OpenAI’s format, just routed somewhere else.

Other Rig provider styles

TODO: Incomplete – only style = "openai" is wired up in v0. Native "anthropic" (which would talk to the Anthropic API directly rather than via an OpenAI-compatible bridge), Cohere, etc. are pending.

[providers.anthropic]
style    = "anthropic"
base-url = "https://api.anthropic.com/v1"
api-key  = "${ANTHROPIC_API_KEY}"

In-process providers (mistralrs)

An in-process provider runs the model in the outrig process itself, with no socket and no serialization. The use case is questions whose content must not leave the host – the eventual egress filter, tool-use filter, and prompt-injection scanner all want this. See In-process LLMs for the full picture.

A mistralrs provider table is bare – it just declares “this is the in-process runtime.” Each set of weights goes on a [models.<name>] row referencing the provider:

[providers.local]
style = "mistralrs"

# Auto-download from HuggingFace on first use:
[models.phi3-fast]
provider   = "local"
model-id   = "microsoft/Phi-3-mini-4k-instruct-gguf"
model-file = "Phi-3-mini-4k-instruct-q4.gguf"

# Or point at a GGUF you placed on disk yourself:
[models.llama-local]
provider   = "local"
model-path = "/var/cache/outrig/models/llama-3-8b-instruct.q4.gguf"

The provider takes no base-url and no api-key. Each mistralrs model must set exactly one of model-id / model-path. One provider can back many models.

The backend is gated behind cargo build --features local-llm. A build without the feature still parses and validates style = "mistralrs" blocks cleanly; the error fires only when an agent tries to actually use one of those models, with a message that names the missing feature flag. This keeps configs portable across builds.

For the full schema (including revision, context-length, and the top-level model-cache-root key) see Reference -> Config. For why this exists at all – and why “localhost LLM” isn’t the same thing – see In-process LLMs.

Tool calling

outrig’s agent loop relies on the model emitting tool calls in the provider’s native format (e.g. OpenAI’s tool_calls field). Models that don’t support tool calling won’t work – the agent will appear to “see” tools in its prompt but never invoke them.

If you’re using an OpenAI-compatible endpoint, verify the underlying model supports tool/function calling before pointing outrig at it. A quick test: run a one-shot prompt asking the model to “list files in /workspace” and watch for [outrig] tool call: fs__list_directory(...) on stderr. No tool-call line means no tool calling.

API keys are env-var-only

api-key = "${VAR}" is the only accepted form for the API key. outrig refuses to load any other value – a literal key, a missing ${...} wrapper, anything. This guarantees:

  • Configs are safe to commit to source control.
  • Keys never end up in outrig logs output, in tracing diagnostics, or in session metadata on disk.
  • Rotating a key is a shell change, not a file edit.

Different shells (different accounts, different rate limits) just point api-key at different env-var names:

[providers.cheap]
style    = "openai"
base-url = "https://api.together.xyz/v1"
api-key  = "${TOGETHER_API_KEY}"

[providers.expensive]
style    = "openai"
base-url = "https://api.openai.com/v1"
api-key  = "${OPENAI_API_KEY}"

Where things live: global vs repo

LayerGlobal (~/.outrig/config.toml)Repo (.agents/outrig/config.toml)
[providers.<name>]typical homeallowed for repo-only providers
[models.<name>]typical home (reused names)allowed for repo-specific models
[agents.<name>]raretypical home
[workspace]repo only
[images.<name>]repo only
default-modeltypical homeoptional override
default-agentrarerequired for outrig run
default-imagerarerequired for outrig run

If a name is defined in both, the repo wins – override by redefining.

See also

  • Quickstart – shows outrig init writing the repo config and invoking outrig config init for the global one.
  • Reference -> Config – every key in [providers], [models], [agents].
  • Rig documentation – what each provider style Rig ships supports.

In-process LLMs

An in-process LLM provider runs the model in the same address space as the outrig CLI itself, backed by the mistralrs crate. The model weights load into outrig’s process; questions never cross a socket, never get serialized into JSON, never reach a network.

This is a feature you opt into at build time and at config time. Most users running outrig against a hosted API never need it.

Why you might want this

Three downstream features (none of which ship with the in-process provider itself) need an LLM whose input must not leave the host:

  • Network egress filter. A future CONNECT proxy in front of the container will ask “does this outbound payload match what the user told the agent to do?” Asking a remote LLM that question is self-defeating – the payload becomes the question.
  • Tool-use filter. A wrapper around the agent loop will ask “is this tool call, with these arguments, consistent with the agent’s stated objective?” The arguments may include source code, secrets, or session context the user does not want round-tripped to a third party.
  • Prompt-injection scanner. A pre-filter on incoming tool results will ask “does this content appear to contain instructions targeting the agent?” Tool results from untrusted sources are exactly what you don’t want re-emitted to a remote LLM.

In all three cases the answer is small (often a single token, sometimes a structured verdict). It’s the question that’s sensitive. The right placement for the model is local enough that the question never crosses a process boundary.

TODO: Incomplete – the egress filter, tool-use filter, and prompt-injection scanner are downstream features. v0 ships the in-process provider as plumbing only; nothing in outrig calls it automatically yet.

Why in-process and not localhost

A local LLM server (Ollama on 127.0.0.1, for instance) puts the model in another process under the same user. That’s not the same trust boundary as in-process:

  • Marshaling. Sensitive payloads still cross a socket and get serialized into JSON. Any process with the right uid (or ptrace) can observe the traffic. “Did this question ever get serialized somewhere I can’t see?” becomes harder to answer.
  • Lifecycle skew. A separate daemon has its own start/stop, its own logging, its own crash recovery. The trust property “this question was answered locally” is weaker when it depends on another process’s configuration.

In-process keeps the question, the model weights, and the answer in one address space owned by outrig itself. The boundary is the outrig process, not “the host machine.”

If you want the convenience of a localhost server (Ollama, vLLM, etc.) and don’t need the in-process trust property, the OpenAI-compatible style = "openai" provider with a localhost base-url is the right tool. Use the in-process provider only when content locality is itself the requirement.

Configuring an in-process provider

A style = "mistralrs" provider has no base-url and no api-key. The provider table is bare – it just declares “this is the in-process runtime.” Each set of weights goes on its own [models.<name>] row referencing the provider, so one mistralrs provider can back many models. You tell outrig where to find each model’s weights either by HuggingFace repo id (outrig downloads it) or by local path (you place it on disk).

[providers.local]
style = "mistralrs"
[models.phi3-fast]
provider   = "local"
model-id   = "microsoft/Phi-3-mini-4k-instruct-gguf"
model-file = "Phi-3-mini-4k-instruct-q4.gguf"   # required when the repo has multiple GGUFs
# revision      = "main"   # optional; pin a git ref for reproducibility
# context-length = 4096    # optional; override the model's default context window
# device         = "cuda"  # optional; defaults to "cpu"

On first use, outrig downloads the named GGUF file from https://huggingface.co/<model-id> and caches it under <XDG_CACHE_HOME>/outrig/models/ (override with the top-level model-cache-root config key; see Reference -> Config). Subsequent runs reuse the cached file.

model-file is optional only when the repo ships exactly one .gguf. Repos that publish several quantizations (-q4, -q5_k_m, -f16, etc.) require an explicit pick.

From a local path

[models.llama-local]
provider   = "local"
model-path = "/var/cache/outrig/models/llama-3-8b-instruct.q4.gguf"
# context-length = 4096    # optional
# device         = "metal" # optional; defaults to "cpu"

Use this when you want to pre-stage the model yourself – in CI, in air-gapped environments, or when you want to manage the cache directory by hand. model-path may be absolute or relative to the repo root.

One or the other, not both

Exactly one of model-id and model-path is required on each mistralrs model. Specifying both, or neither, is a config error. model-file and revision are only meaningful with model-id. The [models.<name>].identifier field that openai-style models use is not allowed on mistralrs models – the weights are the model.

GGUF only

v0 supports GGUF model files only. mistralrs can also load raw HuggingFace safetensors directories, but outrig doesn’t expose that path – if you need it, file an issue.

Device selection

By default, mistralrs models run on CPU:

device = "cpu"

GPU builds can opt into CUDA or Metal per model:

device = "cuda"    # CUDA device 0
device = "cuda:1"  # CUDA device 1 as the base device
device = "metal"   # Metal device 0

cuda requires a binary built with --features "local-llm cuda"; metal requires --features "local-llm metal". A config that asks for an unavailable backend fails loudly when the agent is resolved, with a rebuild hint. Enabling cuda or metal without local-llm emits a build warning and has no effect. outrig does not silently fall back to CPU, because that would hide the performance and policy properties the user asked for.

Metal is only usable on macOS targets. Non-macOS builds can compile with the metal feature for feature-matrix coverage, but trying to instantiate a Metal device fails with a platform error.

For one-off runs, outrig run --device cuda, --device cuda:1, --device metal, or --device cpu overrides the model’s configured device without editing the config file. The override only applies to style = "mistralrs" models.

outrig still passes mistralrs’s automatic device map through to the loader. For cuda:N, N is the selected base device; mistralrs may use other same-kind devices if its auto mapper decides the model needs them. Explicit sharding controls and ROCm/AMD GPU support are not part of this surface yet.

Build flag and the “still parses” rule

The in-process backend is gated behind a Cargo feature:

cargo build --features local-llm
cargo build --features "local-llm cuda"
cargo build --features "local-llm metal"

A build without --features local-llm still recognizes style = "mistralrs" in config files. Parsing succeeds, cross-reference validation succeeds, outrig will load and display configs that contain mistralrs-style providers and models without complaint. The error fires only when an agent actually tries to use one of those models – at agent-resolve time, when outrig walks agent -> model -> provider and tries to instantiate a client. The message names the missing feature flag so the fix (“rebuild with --features local-llm”, --features cuda, or --features metal) is one shot.

The point of this design is portability: a repo’s .agents/outrig/config.toml can declare both an OpenAI-style provider and a mistralrs-style provider, and the same checked-in config works for teammates whether or not they built with the feature on. The cost – “using a mistralrs model on a build that doesn’t support it errors out at run time” – is paid only by users who actually try to use it.

First-use download stalls

The first request to a model-id provider blocks while the GGUF downloads. Models are typically a few hundred megabytes to a few gigabytes; on a residential connection this can take minutes. There is no progress UI in v0; the run looks idle until the download completes. Pre-warm by running outrig once with a short prompt before relying on it for real work, or use the local-path form and place the file yourself.

Decode streaming

After the model is loaded, assistant replies stream to stdout while mistralrs decodes them. This matters most on CPU, where a long local reply can take minutes if you wait for the full completion. Tool-call traces and prompts remain on stderr, so outrig run > reply.txt still captures only assistant text.

Model lifecycle

Loading a GGUF is expensive (seconds, sometimes tens of seconds, sometimes gigabytes of RAM). outrig holds one loaded engine per model name for the lifetime of the process:

  • The first request to a mistralrs model triggers the load.
  • Subsequent requests against the same model reuse the loaded engine.
  • Two agents that point at the same [models.<name>] block share one in-memory copy.
  • Two [models.<name>] rows that share a provider but specify different weight specs load distinct engines – the cache key is the model name, not the provider.
  • The engine is dropped on outrig process exit. There’s no eviction in v0 – one engine per model, no multi-tenant pressure.

The registry lives in the host outrig process, never inside the sandboxed container. Loading the model in the container would defeat the trust property: the container is the thing being filtered.

flowchart LR
    you(["you<br/>(terminal)"])
    api(["remote LLM<br/>(HTTPS API)"])

    subgraph host["host -- outrig process"]
        direction TB
        outrig["outrig CLI"]
        rig["Rig agent loop"]
        registry["LlmRegistry<br/>(in-process models)"]
        outrig --> rig
        rig --- registry
    end

    subgraph container["podman container"]
        mcp["MCP servers"]
    end

    you -- "prompt" --> outrig
    rig <-- "remote model" --> api
    rig -- "tool call" --> mcp
    registry -. "(future) policy" .-> rig

What this enables (sketch)

The downstream features named under “Why you might want this” will share a small one-shot policy API:

#![allow(unused)]
fn main() {
pub struct PolicyEngine { /* ... */ }

impl PolicyEngine {
    /// Ask a yes/no policy question; returns the verdict.
    pub async fn classify_yes_no(&self, question: &str) -> Result<bool>;

    /// Ask a structured question; returns a JSON value validated against the schema.
    pub async fn classify_json(
        &self,
        question: &str,
        schema: &serde_json::Value,
    ) -> Result<serde_json::Value>;
}
}

The implementation will lean on mistralrs’s constrained-JSON decoding so the returned value is well-formed by construction. The point of mentioning this here, before any of it ships, is so the “what is this provider for?” question has a concrete answer.

See also

  • Providers, Models, and Agents – the three-layer LLM config; the in-process provider plugs into the same [providers.<name>] slot as a remote one.
  • Reference -> Config – field-level schema for the bare style = "mistralrs" provider, the matching mistralrs [models.<name>] rows, and the top-level model-cache-root key.
  • Workspace – the eventual egress filter is the headline consumer of this provider.

Usage

This section is the day-to-day operator’s guide: what each subcommand does, what the REPL looks like in practice, what comes back when something goes wrong.

  • outrig init – one-shot setup orchestrator: runs config init if needed, writes .agents/outrig/config.toml, and loops image add to scaffold image-configs.
  • outrig config – group of commands for outrig’s configuration. v0: config init (writes the global ~/.outrig/config.toml).
  • outrig image – group of commands for image-configs. v0: image add (scaffolds a Dockerfile under .agents/outrig/images/<name>/).
  • AI-assisted design – use outrig mcp self when the built-in image templates do not fit.
  • outrig run – start an interactive agent session. The main subcommand.
  • outrig mcp – expose an image-config’s MCP tools to an external client.
  • outrig build – pre-warm the image cache so the next outrig run is instant.
  • Sessionsoutrig ls, outrig logs, outrig discard, outrig clean.
  • Recipes – common patterns (multiple image-configs, capturing transcripts, scripted single-prompt runs).

For exhaustive flag-by-flag reference, see Reference -> CLI.

outrig init

outrig init is the one-shot setup for a new repo. It’s a thin orchestrator over two more focused commands plus one inline repo-config phase:

  1. If ~/.outrig/config.toml is missing, run outrig config init.
  2. Create .agents/outrig/ and .agents/outrig/config.toml if absent.
  3. Offer to call outrig image add in a loop.

Each phase is idempotent. Re-running outrig init on a fully-set-up repo does nothing for the first two phases; the image-config loop is always offered (you may want to add another image-config later).

Synopsis

outrig init [--force]
FlagDefaultDescription
--forceoffOverwrite existing files. Propagates to config init and image add.

Run it

$ cd hello-outrig
$ outrig init

If ~/.outrig/config.toml doesn’t exist, outrig walks you through providers and models first – the same flow as outrig config init, inlined here so first-time users finish in one session. Every prompt shows the default in [default: ...]; press Enter to accept it. Type ? and Enter at any prompt for an explanation of what’s being asked plus the available options.

[outrig] no global config found at ~/.outrig/config.toml -- let's create one.

? Pick a provider style [default: openai]:
? Provider name (used in models) [default: openai]:
? Base URL [default: https://api.openai.com/v1]:
? API key environment variable [default: OPENAI_API_KEY]:
? Define a model now? [Y/n]:
? Model name (used in agents) [default: fast]:
? Model identifier [default: gpt-4o-mini]:
? Provider for this model [default: openai]:
? Use this model as default-model? [Y/n]:

[outrig] wrote ~/.outrig/config.toml

If the global config already exists, that step is skipped:

[outrig] using existing global config at ~/.outrig/config.toml

Then the repo half:

[outrig] no repo config at .agents/outrig/config.toml -- let's create one.

Configuring models
[outrig] models available in your global config: fast (default: fast)
? Would you like to configure LLM models specific to this repo? [Y/n]:
? Model name [default: fast]:
? Provider for this model [default: openai]:
? Model identifier [default: gpt-4o-mini]:
? Add another model? [y/N]:
? Use this model as default-model? [Y/n]:

Configuring your first agent
? Agent name [default: coder]:
? Preamble (one line, edit later) [default: You are a careful coding assistant.]:

Configuring your first image
? Image-config name [default: hello-outrig-standard]:
? Workspace host-path [default: .]:
? Workspace container-path [default: /workspace]:

[outrig] wrote .agents/outrig/config.toml

The default agent name is coder (a role-based constant). The default image-config name is <repo-folder>-standard (kebab-cased), so the image-config carries the repo’s identity while the agent carries its role.

The model section reads your global ~/.outrig/config.toml and lists the available models. Answering “yes” walks the same model-definition prompts as outrig config init, except the new [models.<name>] entries land in the repo config (referencing the global providers). You can then optionally pin one as the repo’s default-model. “No” inherits everything from the global config.

If your global config has no providers, the section instead prints a hint to run outrig config init and skips the prompt – the agent can’t run without a provider.

If you answer “no” to defining repo-specific models, outrig still offers to pin one of the global models as the repo’s default-model. The default flips: when your global config has a default-model, declining makes sense (inherit it, default [y/N]); when it doesn’t, picking is needed to avoid a config with no resolvable model (default [Y/n]).

If no default-model is set anywhere – globally or at the repo level – the agent prompt forces an explicit model selection. That guarantees the resulting config validates and outrig run will work.

Finally the image-config loop:

? Add an image-config now? [Y/n]:

  ... (calls `outrig image add` -- see that page for the full prompt sequence)

? Add another image-config? [y/N]:

Press Ctrl-C at any prompt to stop; partial files are not written until all answers are gathered.

What gets written

A minimal .agents/outrig/config.toml (image-configs will be filled in by image add):

default-image = "hello-outrig-standard"
default-agent     = "coder"

[workspace]
host-path      = "."
container-path = "/workspace"

[agents.coder]
# inherits default-model from the global config
preamble = "You are a careful coding assistant."

The global ~/.outrig/config.toml is written by outrig config init; the per-image Dockerfile and [images.<name>] block come from outrig image add.

Re-running

Without --force, outrig init skips work that’s already done:

  • Global config present -> skip the config init phase.
  • Repo config.toml present -> skip the repo-config phase.
  • Image-config loop -> always offered, since adding more image-configs later is the expected workflow.

With --force, every nested write rewrites in place. You probably want the more targeted commands instead – outrig config init --force or outrig image add <name> --force.

TODO: Incomplete – non-interactive mode (outrig init --provider openai --model fast ...) is deferred.

See also

outrig config

outrig config groups commands that read and write outrig’s configuration files. In v0 only outrig config init is implemented; the rest of the group (config get, config set, config list) is reserved for later – the same shape as git config.

outrig config init

outrig config init walks you through the global config at ~/.outrig/config.toml (or <XDG_CONFIG_HOME>/outrig/config.toml if XDG_CONFIG_HOME is set) – the user/machine-level providers and models that any outrig repo can reuse.

It’s the first phase of outrig init, which runs config init automatically when no global config is present.

Synopsis

outrig config init [--force]
FlagDefaultDescription
--forceoffOverwrite an existing global config. Without it, outrig refuses to clobber.

Run it

Every prompt shows the default in [default: ...]; press Enter to accept it. Type ? and Enter at any prompt for an explanation of what’s being asked plus the available options.

$ outrig config init
[outrig] writing global config to ~/.outrig/config.toml

? Pick a provider style [default: openai]:
? Provider name (used in models) [default: openai]:
? Base URL [default: https://api.openai.com/v1]:
? API key environment variable [default: OPENAI_API_KEY]:

? Add another provider? [y/N]:

? Define a model now? [Y/n]:
? Model name (used in agents) [default: fast]:
? Provider for this model [default: openai]:
? Model identifier [default: gpt-4o-mini]:

? Add another model? [y/N]:
? Use this model as default-model? [Y/n]:

[outrig] wrote ~/.outrig/config.toml

If you pick mistralrs as the provider style, the provider itself has no follow-up prompts – it’s just a tag. The weight-source prompts (Use auto-download by model ID?, HuggingFace model-id or Local model-path, revision, context-length) are asked once per model in the model loop, since each model carries its own weight spec.

Help at any prompt

Type ? and Enter at any prompt to get a short explanation of the field and its options. The prompt is then re-displayed so you can answer:

? Pick a provider style [default: openai]: ?

  A provider style is the wire format outrig uses to talk to your LLM endpoint.

  openai     OpenAI Chat Completions wire format. Works with OpenAI itself, OpenRouter,
             Together, vLLM, Ollama, and any compatible endpoint.
  anthropic  (TODO: not yet wired in v0) Native Anthropic API.

  See: https://tgockel.github.io/outrig/concepts/llm-providers.html

? Pick a provider style [default: openai]:

The help text comes from the same descriptions used in Reference -> Config; prompts and reference stay in sync.

What gets written

A minimal ~/.outrig/config.toml:

default-model = "fast"

[providers.openai]
style    = "openai"
base-url = "https://api.openai.com/v1"
api-key  = "${OPENAI_API_KEY}"

[models.fast]
provider   = "openai"
identifier = "gpt-4o-mini"

Re-running

Without --force, outrig config init refuses if the global config already exists:

$ outrig config init
error: ~/.outrig/config.toml already exists; pass --force to overwrite.

To change one field, edit the file directly. There is no plan to support partial re-init in v0 – the config is small enough to edit by hand.

TODO: Incomplete – non-interactive mode (outrig config init --provider openai --model fast ...) is deferred.

See also

outrig image

outrig image groups commands for the container images that host an agent’s tools. outrig image add scaffolds a repo-local image-config (a named Dockerfile + MCP-server bundle); outrig image init scaffolds a standalone image project whose build output is a reusable image, outrig image build builds and validates that project, and outrig image inspect reads an image’s declared labels locally by default, or from a registry with --remote. The rest of the group (image ls, image rm) is reserved for later.

outrig image add

outrig image add scaffolds a new image-config. It writes a Dockerfile under .agents/outrig/images/<name>/Dockerfile and appends matching [images.<name>] and [images.<name>.mcp] blocks to your repo’s config.toml.

Run it any time you want to add an image-config – e.g., a planning config alongside the <repo>-standard one outrig init creates by default. init calls image add in a loop for the first (and any further) image-configs you create during initial setup.

Bootstrapping a fresh repo

If you run outrig image add in a directory without an .agents/outrig/config.toml (neither here nor in any parent), outrig prompts before scaffolding:

[outrig] no .agents/outrig/config.toml found in /path/to/repo or any parent.
? Configure outrig in this directory now? [Y/n]:

Answering y walks the same repo-config prompts that outrig init uses (workspace, default agent, preamble), then continues with the image add flow. Answering n exits with the same error a strict find would have produced – run outrig init later when you’re ready.

Synopsis

outrig image add [<name>] [--force]
Argument / flagDefaultDescription
<name>promptedImage-config name (becomes [images.<name>]).
--forceoffOverwrite existing Dockerfile/config entries for this name.

Run it

Every prompt shows the default in [default: ...]; press Enter to accept it. Type ? and Enter at any prompt for an explanation of what’s being asked plus the available options. The default image-config name is <repo-folder>-standard (kebab-cased), so the example below assumes a hello-outrig repo.

$ cd hello-outrig
$ outrig image add
? Image-config name [default: hello-outrig-standard]:
? Base image [default: debian:bookworm-slim]:
? Language toolchains [default: ]: rust, node
? MCP servers [default: fs]:

[outrig] wrote .agents/outrig/images/hello-outrig-standard/Dockerfile
[outrig] added [images.hello-outrig-standard] block to .agents/outrig/config.toml
[outrig] added [images.hello-outrig-standard.mcp] entries: fs

Next: try `outrig build` to verify the image builds, then `outrig run`.

The prompts are intentionally limited – the goal is a known-good starting point you can edit by hand, not an exhaustive Dockerfile generator.

Help at any prompt

? Language toolchains [default: ]: ?

  Pick zero or more language toolchains to install in the image. The Dockerfile
  template adds the corresponding install steps; you can edit the file afterwards.

  rust    rustup + stable toolchain (cargo, rustfmt, clippy).
  node    Node 20 LTS via NodeSource.
  python  CPython 3.12 with pip and venv.
  go      Go 1.22.
  none    Just the base image -- nothing extra installed.

  See: https://tgockel.github.io/outrig/usage/image.html#known-toolchains

? Language toolchains [default: ]:

Known toolchains

The toolchain prompt offers curated options that cover the common cases:

ChoiceWhat gets installed
rustrustup + the stable toolchain, cargo, rustfmt, clippy.
nodeNode 20 LTS via the base image’s package manager (or NodeSource).
pythonCPython 3.12 with pip and venv.
goGo 1.22.
noneJust the base image.

You can pick more than one. The Dockerfile is a starting point – edit it freely afterwards.

Known MCP servers

  • fs – installs @modelcontextprotocol/server-filesystem from npm; default [mcp] entry: { command = ["mcp-server-filesystem", "/workspace"] }.
  • git – installs mcp-server-git from PyPI; default [mcp] entry: { command = ["mcp-server-git", "--repository", "/workspace"] }.

fs is the default. Add git for repo-aware tools, or wire up additional servers by editing the [images.<name>.mcp] block directly – see Concepts -> MCP Servers. A shell MCP server is the usual next tool for coding image-configs; choose a package, install it in the Dockerfile, and declare its command in [images.<name>.mcp].

TODO: Incomplete – the list of “known MCP servers” will grow as the ecosystem does. Anything not listed here you install in the Dockerfile by hand.

When templates do not fit

The prompt flow is intentionally small. If you need an image with a database, internal SDK, unlisted MCP server, or other custom package set, use outrig mcp self. It gives an MCP-capable AI tool the OutRig docs, config schema, suggested tools, and advisory validators so it can propose a Dockerfile and matching [images.<name>] block without being limited to the built-in template menu.

What gets written

.agents/outrig/images/hello-outrig-standard/Dockerfile (excerpt):

FROM docker.io/library/debian:bookworm-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      ca-certificates curl git build-essential passwd \
 && rm -rf /var/lib/apt/lists/*

# rust toolchain
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
       | sh -s -- -y --default-toolchain stable --profile default
ENV PATH=/root/.cargo/bin:$PATH

# node toolchain
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
 && apt-get install -y --no-install-recommends nodejs \
 && rm -rf /var/lib/apt/lists/*

# MCP servers
RUN npm install -g @modelcontextprotocol/server-filesystem

WORKDIR /workspace
CMD ["sleep", "infinity"]

The Dockerfile is generic – no USER directive, no hard-coded UID. outrig sets up a user matching your host UID/GID at run time (see Concepts -> Workspace). The passwd package keeps useradd/groupadd available for that bootstrap step.

Appended to .agents/outrig/config.toml:

[images.hello-outrig-standard]
dockerfile = ".agents/outrig/images/hello-outrig-standard/Dockerfile"
context    = ".agents/outrig/images/hello-outrig-standard"

  [images.hello-outrig-standard.mcp]
  fs = { command = ["mcp-server-filesystem", "/workspace"] }

Re-running

Without --force, outrig refuses if either the Dockerfile path or the config block already exists for that name:

$ outrig image add hello-outrig-standard
error: .agents/outrig/images/hello-outrig-standard/Dockerfile
       already exists; pass --force to overwrite.

With --force, the Dockerfile is replaced and the [images.<name>] block is rewritten in place (preserving surrounding TOML).

outrig image init

outrig image init scaffolds a standalone image project: a self-contained directory whose build output is a reusable container image. Unlike image add, it writes no repo config – it creates three files and nothing else:

  • Dockerfile – a Debian-slim image with the filesystem MCP server, ending in the standard CMD ["sleep", "infinity"]. It installs the tool binaries; it does not copy OutRig config.
  • image.toml – the standalone image authoring config: [image].ref plus a [mcp] table. outrig image build validates this file and stamps the [mcp] table into the image’s OCI labels, which outrig reads back at session startup and merges with any repo [images.<name>.mcp].
  • README.md – how to build the image and reference it from a repo.

The command is noninteractive – there are no prompts. The project name (used as [image].ref) defaults to the target directory’s name.

Synopsis

outrig image init [<dir>] [--force]
Argument / flagDefaultDescription
<dir>current dirProject directory; its name becomes the image ref.
--forceoffOverwrite the generated files if they already exist.

The directory name must match ^[a-zA-Z][a-zA-Z0-9_-]*$ (e.g. rust-dev). A target that resolves to no usable name is rejected; . and the no-argument form use the current directory’s name.

What gets written

$ outrig image init rust-dev
[outrig] wrote rust-dev/Dockerfile
[outrig] wrote rust-dev/image.toml
[outrig] wrote rust-dev/README.md
[outrig] next: build this image with `outrig image build`

rust-dev/image.toml:

[image]
ref = "rust-dev"

[mcp]
fs = { command = ["mcp-server-filesystem", "/workspace"] }

[build] is omitted, so the build defaults to the sibling Dockerfile with context ..

Consuming the image from a repo

Once built, reference the image from a repo’s config.toml with image-name. Because the MCP server declarations are stamped into image labels, the consuming block needs no [images.<name>.mcp]:

[images.rust-dev]
image-name = "rust-dev"

That authoring split is the key difference from image add: repo-local blocks keep their [images.<name>.mcp] entries in config.toml and stamp them into cache image labels when built; standalone projects carry the declarations in image.toml so the image can be consumed elsewhere without repo-local MCP config. See Concepts -> Containers for the image-name shape.

Re-running

Without --force, init refuses if any of the three files already exists, naming the conflicts:

$ outrig image init rust-dev
error: rust-dev/Dockerfile, rust-dev/image.toml, rust-dev/README.md
       already present; pass --force to overwrite.

--force regenerates the three files and leaves anything else in the directory untouched.

outrig image build

outrig image build builds a standalone image project (the output of outrig image init) and validates that the result is a working OutRig toolset image. It is the standalone-project counterpart to outrig build, which builds repo-local image-configs.

It reads the project’s image.toml, serializes it into OCI labels, builds the declared Dockerfile with buildah, and tags the result with the [image].ref from image.toml – stamping those labels onto the image. Then it always validates the built image, and – unless --no-test – live-tests every MCP server the image declares.

Synopsis

outrig image build [<dir>] [--tag <ref>] [--no-test] [--no-cache]
Argument / flagDefaultDescription
<dir>current dirProject directory holding image.toml.
--tag <ref>[image].refBuild tag override; does not edit image.toml.
--no-testoffSkip the live MCP test; still validates image.toml.
--no-cacheoffForce a clean build; passed through to buildah.

What it does

$ outrig image build rust-dev
[outrig] building image rust-dev
[outrig]   dockerfile: Dockerfile
[outrig]   context:    .
... buildah build output ...
[outrig] image ready
[outrig] image config validated (1 mcp server)
[outrig] mcp fs: initialized (12 tools)
[outrig] image ok

After the build, outrig reads the stamped org.outrig.mcp label back off the built image and validates it (every server name well-formed, every command non-empty). By default it then starts the image, and for each declared MCP server initializes it and calls tools/list, reporting the tool count per server. The image is built locally; nothing is pushed.

Failure modes

The command exits nonzero – printing error: ... – in these cases:

  • image.toml missing, malformed, or missing required fields (no [image].ref, an empty or invalid [mcp] table, a partial [build]): caught before the build runs.
  • The stamped org.outrig.mcp label is missing or malformed: read back off the built image and validated after the build, even with --no-test.
  • A declared MCP server cannot initialize or return tools/list: caught during the live test. --no-test skips this check (the per-server stderr is captured in the error message).

--no-test

--no-test skips only the live MCP server test. The stamped org.outrig.mcp label is still read back from the built image and validated, so a missing or malformed config still fails. Use it to verify a build quickly, or in environments where starting the servers is not wanted.

--tag and --no-cache

--tag <ref> tags the build output as <ref> for that invocation instead of the [image].ref in image.toml; the file is never rewritten. Short refs are passed to buildah verbatim, so both rust-dev and rust-dev:0.1.0 work.

Unlike repo-local outrig build – which tags <image-config-name>:<content-hash> and skips the build on a cache hit – a standalone build tags a stable, caller-named ref with no content hash, so there is no project-level cache to skip: --no-cache only forwards --no-cache to buildah to force a clean build.

outrig image inspect

outrig image inspect prints the OutRig config labels declared by an image. It is read-only: local mode checks the local image store and reads labels with podman image inspect; remote mode uses skopeo inspect against a registry. Neither mode pulls image layers, creates a container, or starts an MCP server. Live initialization and tools/list checks belong to outrig image build.

Synopsis

outrig image inspect [--remote] <ref>
ArgumentDescription
<ref>Image ref to inspect. Never pulled.
--remoteRead registry metadata with skopeo inspect.

Output

The command writes only the inspect result to stdout. Diagnostics and errors go to stderr.

$ outrig image inspect rust-dev
image: rust-dev
description: Rust tooling
version: 0.1.0
tags: ["rust","build"]
mcp:
  fs:
    command: ["mcp-server-filesystem","/workspace"]
  db:
    command: ["db-mcp"]
    env:
      TOKEN: "${DB_TOKEN}"

description, version, and tags appear only when the image carries those labels. If the image has no org.outrig.mcp label, inspect still prints the image ref and any metadata labels it finds, then omits the mcp: section.

Repo-local images built by outrig build, outrig run, or outrig mcp are stamped with an org.outrig.mcp label on cache misses. For those <image-config-name>:<hash> refs, inspect shows the merged MCP declarations from inherited/Dockerfile labels plus [images.<name>.mcp], without starting a container. Standalone images may additionally show description, version, and tags.

Use --remote when the ref is in a registry and you want the labels without pulling it:

outrig image inspect --remote ghcr.io/acme/rust-dev:latest

Remote inspection requires skopeo on PATH. Authentication uses the normal container credentials that skopeo reads, such as credentials created by skopeo login, podman login, buildah login, or docker login.

Failure modes

The command exits nonzero – printing error: ... – in these cases:

  • The image is not present locally: inspect reports a local-only not-found error and does not attempt a pull.
  • --remote is used but skopeo is not installed: inspect reports the missing runtime dependency.
  • The registry request fails: skopeo errors, including missing credentials or denied access, are reported with the command and stderr tail.
  • The remote ref uses an unsupported transport: use a plain registry ref or docker://<ref>.
  • A declared OutRig label is malformed: invalid org.outrig.tags or org.outrig.mcp values fail before anything is started.

See also

AI-assisted design

When the built-in templates do not fit, attach outrig mcp self to an MCP-capable AI tool and ask it to design an image-config. The server exposes OutRig’s docs, config schema, curated suggestions, and advisory validators over stdio. It cannot write files, run builds, or mutate your repo; the AI can use those read-only tools before writing through its own client, or it can return the exact file contents for you to install.

Run the server

outrig mcp self is a host-side self-description server. It does not start a container, create a session, or require a repo config.

outrig mcp self

The command is meant to be launched by an MCP client. Keep stdout reserved for MCP protocol messages; status and diagnostics go to stderr.

Client setup

Use an absolute path to outrig if your client starts MCP servers from a different working directory than your shell.

Claude Code:

claude mcp add outrig-self -- outrig mcp self

Claude Desktop:

{
  "mcpServers": {
    "outrig-self": {
      "command": "outrig",
      "args": ["mcp", "self"]
    }
  }
}

Codex CLI:

[mcp_servers.outrig-self]
command = "outrig"
args = ["mcp", "self"]

Cursor:

{
  "mcpServers": {
    "outrig-self": {
      "type": "stdio",
      "command": "outrig",
      "args": ["mcp", "self"]
    }
  }
}

Regenerate any of these snippets with:

outrig design prompt --print-mcp-config <tool>

The valid <tool> names are claude-code, claude-desktop, codex, and cursor.

What the AI sees

The server exposes these tools:

ToolWhat it returns
list_docsEmbedded doc pages with titles and summaries.
get_docMarkdown for one embedded page.
get_config_schemaJSON Schema plus path and image-label hints.
list_base_imagesBase-image suggestions, explicitly non-exhaustive.
list_mcp_server_suggestionsMCP server suggestions and shell guidance.
validate_dockerfileAdvisory warnings about OutRig Dockerfile conventions.
validate_configTOML parse and config validation results for image blocks.
validate_image_tomlTOML validation for standalone image projects.

The suggestion tools are not a registry. The AI can pick any base image, package set, or MCP server command that fits the job. In particular, OutRig supports shell MCP servers even when the suggestion list does not name a single maintained package recipe. The Dockerfile validator is advisory for the same reason: it warns about common OutRig conventions without rejecting custom images.

Suggested prompt

Ask for the files you want and tell the AI to validate both artifacts before it reports done:

Design an OutRig image-config for a Rust and Postgres development environment.
Use the filesystem MCP server at /workspace and add a custom MCP server that runs pg-dump-mcp.
Read the OutRig docs and schema first, then validate the proposed Dockerfile and TOML.
If your client can edit this repo, write the Dockerfile and [images.<name>] block directly.
Otherwise return the exact file paths and file contents.

If an AI client is not allowed to write under .agents/outrig, have it return the exact file contents rather than staging files elsewhere and asking for a blind copy into the repo.

Standalone image projects

For a reusable image that other repos consume by tag, ask for a standalone project instead of a repo-local [images.<name>] block. The output should be a directory with Dockerfile, image.toml, and README.md.

Design an OutRig standalone image project for a Rust and git development toolset.
Return complete contents for Dockerfile, image.toml, and README.md.
Use the filesystem MCP server at /workspace and the git MCP server for /workspace.
Read the OutRig docs and schema first, then validate the proposed Dockerfile and image.toml.
Explain how a repo should reference the built image with image-name.

The standalone image.toml is the authoring source. It must contain [image].ref and a non-empty [mcp] table; optional [image] metadata (description, version, tags) and an optional complete [build] section can be added when useful. outrig image build validates that file and stamps its config into OCI labels, so the Dockerfile should install the tools and MCP binaries but should not copy image.toml into the image.

Trust model

OutRig expects MCP servers to be useful inside the container, not artificially narrow. A filesystem server can point at /workspace or another broad container path; a shell server can run commands inside the container. See MCP Trust Model for the boundary this relies on.

Without MCP

When your AI tool cannot attach MCP servers, print the same design context as a single prompt:

outrig design prompt | pbcopy

# Linux:
outrig design prompt | xclip -selection clipboard

Paste that prompt into ChatGPT, Claude.ai, or any chat UI, followed by what you want the image-config to do. The output is intentionally self-contained: OutRig’s version, embedded docs, conventions, and worked examples are all in the prompt.

To inspect or edit the prompt before sending it:

outrig design prompt > prompt.txt
outrig design prompt --standalone > standalone-prompt.txt

Prefer the MCP path when your tool supports it. outrig mcp self lets the AI read docs and run validators as it iterates; outrig design prompt is a one-shot fallback for clients that only accept pasted text.

outrig run

outrig run is the main subcommand. It walks up from the current directory to find .agents/outrig/config.toml, builds (or cache-hits) the container image, starts the container, attaches every MCP server defined for the selected image, and drops you into a stdin/stdout REPL with the agent.

It can also run config-less: in a directory with no .agents/outrig/config.toml, it uses the current directory as the workspace and reads the agent from the global config. See Config-less runs.

Synopsis

outrig run [--agent <name>]
           [--image <name-or-local-ref>]
           [--config <path>]
           [--device <cpu|cuda|cuda:N|metal>]
           [--max-tool-calls <n>]
           [--max-tool-result-bytes <n>]
           [--model <name>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--session-root <path>]
           [--volume <host:container[:ro|rw]>]
           [--verbose]
  • --agent <name> (default: default-agent): selects an [agents.<name>] block.
  • --image <name-or-local-ref> (default: agent’s image, else default-image): pick an image-config by name; if an explicit --image value does not match config, treat it as a local Podman image ref and run it without pulling.
  • --config <path> (default: walks up from cwd; if not found, run config-less – see Config-less runs): load config from a non-standard location.
  • --device <cpu|cuda|cuda:N|metal> (default: mistralrs model device, else cpu): override the in-process mistralrs model device for this run.
  • --max-tool-calls <n> (default: resolved tool-call-max, else 50): override the per-turn tool-call max for this run.
  • --max-tool-result-bytes <n> (default: resolved tool-result-max, else 262144): override the per-tool-result byte max for this run.
  • --model <name> (default: agent’s model, else default-model): select an existing [models.<name>] entry for this run.
  • --network <default|audit|filter> (default: config [network].mode, else default): choose Podman’s default networking, network audit logging, or global network filtering for this run.
  • --session-dir <path> (default: <session-root>/<sid>): this run’s specific session directory; symlinked from the root.
  • --session-root <path> (default: session-root config, else XDG): root directory containing all sessions.
  • --volume <host:container[:ro|rw]> (repeatable): bind an extra host directory into the container, on top of the default workspace mount. Access defaults to read-only; append :rw for read-write. The host directory must exist; relative host paths resolve against the workspace root.
  • --verbose (default: off): adds buildah/podman command transcripts to stderr and container.log.

Repeat --verbose (-vv) to also enable trace-level logs from outrig’s own modules for that invocation.

Normal startup progress is always printed to stderr so slow container or MCP startup is visible. --verbose adds the underlying buildah/podman command transcript; it is not required for the progress lines. Tracing filters come from OUTRIG_LOG first, then RUST_LOG if OUTRIG_LOG is unset.

When --session-dir is given, outrig writes this run’s session.json and logs/ directly into <path> and creates a symlink at <session-root>/<sid> -> <path> so outrig ls/logs/discard keep working. This lets you launch with a known path and read session.json immediately without looking up an auto-generated id:

$ outrig run --session-dir /tmp/my-debug-run < prompts.txt
$ cat /tmp/my-debug-run/session.json   # known location, no id lookup needed

--session-dir refuses if the path already contains a session.json.

Config-less runs

You can run in a directory with no .agents/outrig/config.toml at all:

$ cd /tmp/scratch
$ outrig run --image my-tool:latest --volume "$PWD/data:/data:rw"

With no repo config found and no --config, outrig uses the current directory as the workspace root (mounted at /workspace) and reads the agent, model, and provider from the global config (~/.outrig/config.toml, or $XDG_CONFIG_HOME/outrig/config.toml). Because there is no default-image, you must pass --image; an unknown ref is used as a local Podman image and is never pulled. If the global config has no resolvable agent, startup fails with the usual no --agent and no default-agent configured error.

What happens, in order

  1. Locate config. Walks up from the current directory until .agents/outrig/config.toml is found. If none is found and no --config is given, run config-less: the current directory becomes the workspace root and config comes from the global file only (see Config-less runs).
  2. Resolve image. Uses explicit --image first. If that value matches [images.<name>], OutRig uses the config block; otherwise it must already exist in local Podman images. Without explicit --image, agent image and default-image still name config blocks only.
  3. Build/cache/probe the image. Config build images run buildah build or cache-hit. Config image-name images may be pulled. Raw --image refs are local-only and are checked with podman image exists.
  4. Start the container. podman run -d --rm --name outrig-<sid> -v <repo>:/workspace:rw --userns=keep-id ... <image> sleep infinity. Any [workspace.mounts] and --volume entries become additional -v binds.
  5. Bootstrap the user. As in-container root, ensure a group with $(id -g) and a user with $(id -u) exist (creating them via groupadd/useradd if not), and that /home/<user> exists and is owned by them. See Concepts -> Workspace for the full logic.
  6. Start network interception, if enabled. --network audit, --network filter, or matching [network].mode config installs the per-session interceptor and opens <session_dir>/logs/network.jsonl. Filter mode also enforces global [network] policy. The default mode skips this step entirely.
  7. Connect MCP servers. For each entry in [images.<name>.mcp], podman exec -i --user=$(id -u):$(id -g) the configured command, run the MCP initialize handshake, and discover tools via tools/list.
  8. Resolve agent -> model -> provider. From --agent (or default-agent), pick the model from --model, else [agents.<a>].model, else top-level default-model. --model must name an existing [models.<name>] entry; it is not a raw provider model identifier. Then [models.<m>].provider, then [providers.<p>]. Resolve the per-turn tool-call max from tool-call-max and the per-result byte max from tool-result-max. For in-process mistralrs models, --device overrides the model’s configured device for this run. For OpenAI-style models, --device is rejected. Then read the API key from the env var named in the provider’s api-key. Build the Rig provider client.
  9. Build the Rig agent. Dynamic tools from every MCP server’s tool list (each prefixed <server>__<tool>), the agent’s preamble and sampling params, assembled with AgentBuilder.
  10. Open the REPL. Banner on stderr, > prompt, ready for input.

If anything before step 10 fails, outrig run reports the error on stderr and exits non-zero without starting the REPL.

REPL banner

A typical startup looks like:

[outrig] loading config
[outrig] config loaded (1ms)
[outrig] resolving agent and image-config
[outrig] agent/image-config resolved: agent coding, image-config coding (0ms)
[outrig] computing image tag
[outrig] image tag computed: coding:8c2a4f7e91d6b5a3 (32ms)
[outrig] ensuring image coding:8c2a4f7e91d6b5a3
[outrig] image ready: coding:8c2a4f7e91d6b5a3 (cache hit) (18ms)
[outrig] starting container outrig-20260502T103412-3f2a
[outrig] container ready: outrig-20260502T103412-3f2a (620ms)
[outrig] bootstrapping container user
[outrig] container user ready (141ms)
[outrig] reading and merging MCP configuration
[outrig] MCP configuration ready: 2 servers (74ms)
[outrig] MCP fs: initializing
[outrig] MCP fs: initialized (188ms)
[outrig] MCP fs: listing tools
[outrig] MCP fs: tools ready: 3 tools (11ms)
[outrig] building agent
[outrig] agent ready (0ms)
[outrig] agent:             coding (model: fast / provider: openai / gpt-4o-mini)
[outrig] tool-call max:     50
[outrig] tool-result max:   262144 bytes
[outrig] image-config:  coding
[outrig] image:             coding:8c2a4f7e91d6b5a3
[outrig] container started: outrig-20260502T103412-3f2a
[outrig] mcp fs:    initialized (3 tools)
[outrig] mcp shell: initialized (1 tool)
[outrig] tools available: fs__read_file, fs__list_directory, fs__write_file, shell__exec
[outrig] session id: 20260502T103412-3f2a   (Ctrl-D to exit, /help for slash commands)
[outrig] entering REPL
>

All startup progress and the banner are on stderr. The only thing that ever goes to stdout is the assistant’s natural-language reply. For in-process mistralrs models, that reply is flushed as chunks while the model decodes; OpenAI-compatible providers print the reply when the turn finishes. The stream separation makes it easy to capture just the model output:

$ echo "summarise this repo" | outrig run > summary.txt

summary.txt ends up with only the model’s text, nothing else.

REPL behavior

Each line you type at > is one user turn:

> add a doc comment to the public function `parse_config` and run cargo check.
[outrig] tool call: fs__read_file({"path": "/workspace/src/config.rs"})
[outrig] tool call: fs__write_file({"path": "/workspace/src/config.rs", ...})
[outrig] tool call: shell__exec({"cmd": "cargo check"})
Done. I added a `///` doc comment describing the function's inputs and the
TOML keys it expects. `cargo check` passed with no warnings. Diff:

  [diff snippet]
>

Behind the scenes the agent may make many tool calls per turn – Rig drives the model-tool-model loop until the model emits a normal text reply with no tool calls. Tool-call traces appear on stderr; assistant text is printed on stdout.

Each turn has a tool-call max. The compiled-in default is 50; a top-level tool-call-max in config changes the default, [agents.<name>].tool-call-max overrides it for one agent, and outrig run --max-tool-calls <n> overrides both for the current run. The max is per turn, so a follow-up prompt starts a fresh count.

Each individual tool result also has a byte max before it is appended to the LLM-visible conversation history. The compiled-in default is 262144 bytes (256 KiB); a top-level tool-result-max in config changes the default, [agents.<name>].tool-result-max overrides it for one agent, and outrig run --max-tool-result-bytes <n> overrides both for the current run. If a tool returns more than the max, outrig keeps the head of the result and appends a marker that includes the original size, the max, and a hint to narrow the next query.

When the tool-call max fires, outrig ends the current turn and keeps protocol-valid partial conversation history. If the model had already asked for the next tool call, outrig records a tool result saying that call was not executed because the max was reached:

[outrig] tool-call iteration max (50) reached; ending turn
[outrig] partial history retained -- send another prompt (e.g. "continue")
        to keep going, or "/reset" to drop it.

Type continue, or any more specific instruction, to let the next turn pick up from the retained tool calls with a fresh max. If the skipped tool call is still needed, the model can ask for it again. Use /reset first when you want to drop that history.

The REPL is line-buffered. Multi-line input is not supported in v0.

TODO: Incomplete – multi-line / paste-mode input is deferred.

Slash commands

Anything starting with / is a REPL command, not a model prompt:

CommandEffect
/helpPrint the slash-command list to stderr.
/toolsList every tool currently registered with the agent, with descriptions.
/resetClear conversation history; container and MCP servers stay up.
/quitExit cleanly (same as EOF/Ctrl-D).
> /tools
[outrig] tools available (4):
  fs__list_directory   List the contents of a directory.
  fs__read_file        Read a file's contents.
  fs__write_file       Write a file (overwrites existing).
  shell__exec          Run a shell command and return stdout/stderr.
> /reset
[outrig] history cleared
>

Interrupting and exiting

  • Ctrl-C during a turn cancels the in-flight LLM/tool call. The REPL prints [outrig] interrupted to stderr and returns to a > prompt with conversation history intact, so you can redirect the agent.
  • Ctrl-D at an empty prompt ends the session: closes MCP server stdios, stops the container, finalizes the session record, exits.
  • A second Ctrl-C without an intervening prompt also exits.
> please refactor everything   ^C
[outrig] interrupted
> never mind, just summarise the file types in this repo.
...

When something goes wrong

Common failures and what they look like:

$ outrig run
error: no .agents/outrig/config.toml found in current directory or any parent
help: run `outrig init` to initialize

You’re not inside an outrig-configured repo. Either cd into one or pass --config <path>.

$ outrig run
[outrig] image-config: coding
error: image-config "coding" is missing required key: dockerfile

The selected [images.<name>] block is incomplete. See Reference -> Config.

$ outrig run
[outrig] image-config: coding
[outrig] image:            coding:8c2a4f7e91d6b5a3 (cache hit)
[outrig] container started: outrig-20260501T134412-3f2a
[outrig] mcp fs: error: failed to spawn `mcp-server-filesystem` in container
[outrig] caused by: exec: "mcp-server-filesystem": executable file not found in $PATH

The MCP server binary isn’t in the image. Install it in the Dockerfile.

$ outrig run --image outrig-standard:53e082e721df8ecc
error: --image "outrig-standard:53e082e721df8ecc" did not match any [images.<name>] and local podman image "outrig-standard:53e082e721df8ecc" was not found

An explicit --image ref that is not an image-config is local-only. Build, tag, or pull it with Podman first, then rerun the command.

$ outrig run
[outrig] image-config: coding
[outrig] image:            coding:8c2a4f7e91d6b5a3 (cache hit)
[outrig] container started: outrig-20260501T134412-3f2a
[outrig] mcp fs: initialized
[outrig] mcp shell: initialized
[outrig] model: gpt-4o-mini
[outrig] session id: 20260501T134412-3f2a
> hi
error: LLM call failed: 401 Unauthorized
caused by: provider returned: invalid_api_key

OPENAI_API_KEY is unset, expired, or pointing at the wrong endpoint.

See also

outrig mcp

outrig mcp turns an outrig image-config into one MCP server for an external client. It starts or attaches to the selected container, launches every [images.<name>.mcp] backing server inside it, and republishes their tools over this process’s stdio by default, or over Streamable HTTP when --listen is set.

Use outrig run when you want outrig to be the LLM client: it resolves an agent, builds a Rig agent, and opens the built-in REPL. Use outrig mcp when another program is the LLM client – Claude Code, Cursor, Zed, or any MCP-capable editor – and you want that program to drive the tools inside your outrig container.

Synopsis

outrig mcp [--image <name-or-local-ref>]
           [--attach <session-id-or-container-name>]
           [--listen <addr>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--config <path>]
           [--global-config <path>]
           [--session-root <path>]
           [--volume <host:container[:ro|rw]>]
           [--verbose]

outrig mcp show-merged [--image <name-or-local-ref>]
                       [--attach <session-id-or-container-name>]

outrig mcp self
  • --image <name-or-local-ref> (default: default-image): selects a [images.<name>] block. If an explicit value does not match config, it is treated as a local Podman image ref and is never pulled. Required with --attach <podman-name>.
  • --attach <session-id-or-container-name> (default: off): reuse an existing container instead of starting one.
  • --listen <addr> (default: off): serve Streamable HTTP at /mcp instead of stdio. Accepts TCP socket addresses such as 127.0.0.1:7331 or 0.0.0.0:7331, plus Unix sockets as unix:/tmp/outrig.sock.
  • --network <default|audit|filter> (default: config [network].mode, else default): choose Podman’s default networking, network audit logging, or global network filtering for this fresh session.
  • --session-dir <path> (default: <session-root>/<sid>): writes to a known path.
  • --config <path> (default: walks up from cwd; if not found, run config-less): path to repo config.toml.
  • --global-config <path> (default: ~/.outrig/config.toml): path to global config.
  • --session-root <path> (default: config, then XDG data directory): root for all sessions.
  • --volume <host:container[:ro|rw]> (repeatable): bind an extra host directory into the container, on top of the default workspace mount. Read-only unless :rw is given; the host directory must exist. Rejected with --attach.
  • --verbose (default: off): adds buildah/podman command transcripts to stderr and container.log.

outrig mcp self is different from the session MCP server. It does not resolve a repo config, start a container, or create a session. It serves OutRig’s own docs, schema, suggestions, and advisory validators so an external AI tool can design an image-config. See AI-assisted design.

There is no --agent flag. outrig mcp has no agent, so it never consults default-agent, agent.image, [agents], [models], [providers], or provider API keys. Image selection is only:

  1. --image <name-or-local-ref>
  2. top-level default-image

If neither is set, startup fails with:

error: no --image or default-image configured

default-image must name a config block. The raw local-image fallback applies only to explicit --image values and to raw image refs saved in session records.

outrig mcp can also run in a directory with no .agents/outrig/config.toml (and no --config): it uses the current directory as the workspace root and merges in the global config. Since there is no agent, all you need is --image <local-ref>; the proxied MCP servers come from the image’s org.outrig.mcp labels.

With --attach, image-config selection is different:

  1. If the attach value matches an exact session id under the resolved session root, outrig reuses that session row’s container_name and image_config_name.
  2. If --image <name-or-local-ref> is also passed, it overrides the session row’s image_config_name.
  3. If the attach value is not a known session id, outrig treats it as a podman container name and requires --image <name-or-local-ref>.

--network audit and --network filter are rejected with --attach; borrowed containers are not retrofitted with a new interceptor.

The selected image must expose at least one backing MCP server after image org.outrig.mcp label entries and [images.<name>.mcp] overrides are merged. Repo-local build cache images already stamp that merged table into their labels on cache misses, but startup still applies the same merge. An image with no merged entries has nothing to proxy, so outrig mcp exits before the client sees an MCP initialize response.

Minimal Config

outrig mcp can run from an image-only repo config:

default-image = "coding"

[workspace]
root = "."

[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"
context    = ".agents/outrig/images/coding"

[images.coding.mcp]
fs    = ["mcp-server-filesystem", "/workspace"]
shell = ["bash", "-lc", "exec shell-mcp-command"]

The MCP server binaries still have to exist inside the image. Install them in the container Dockerfile just as you would for outrig run. In the example above, shell-mcp-command stands for whichever shell MCP server package you choose to install.

An image can also carry the same [mcp] table in its org.outrig.mcp OCI label. Use that when a shared image owns the default tool set, then keep only repo-specific overrides in .agents/outrig/config.toml. See Concepts -> MCP Servers.

To inspect the effective table without serving MCP:

outrig mcp show-merged --image coding

In fresh mode this starts the selected container, reads the image’s org.outrig.mcp label, applies config.toml overrides, prints the merged [mcp] table to stdout, then stops the container. For repo-local build images, the cache tag was already stamped with that merged label when it was built. With --attach, it borrows the existing container for the same read and leaves it running.

Attach Mode

Use attach mode when a container is already running and you want an external MCP client to share its workspace, installed tools, and environment:

outrig mcp --attach 20260504T141907-a83f
outrig mcp --attach outrig-20260504T141907-a83f --image coding

The first form resolves an existing outrig session id. The second form borrows a podman container directly, which is useful for containers not started by outrig run.

Attach mode shares the container, not the MCP protocol processes. The attacher starts its own podman exec -i children for each merged MCP server, so the external client has independent MCP state and independent stderr logs. Existing MCP children from the host session keep running.

Every attach invocation writes its own fresh session row and log directory. outrig logs <attached-session> <server> therefore works the same as it does for a fresh-container outrig mcp session.

MCP servers used with attach mode should be reentrant-safe. Servers that bind a fixed port, write a global pidfile, or take an exclusive lock can conflict with the host session’s copy or with another attacher.

Client Configuration

Put outrig on PATH, or use an absolute path to the binary in each client config. MCP clients may start servers with a different working directory than your shell, so passing an absolute --config path is the least surprising setup.

Claude Code can add a stdio server from the command line:

claude mcp add --transport stdio outrig -- outrig mcp \
  --config /path/to/repo/.agents/outrig/config.toml \
  --image coding

Claude Code and Cursor both understand an mcpServers JSON shape. For Cursor, place this in .cursor/mcp.json for project scope or ~/.cursor/mcp.json for global scope. For Claude Code project scope, use .mcp.json in the project root.

{
  "mcpServers": {
    "outrig": {
      "type": "stdio",
      "command": "outrig",
      "args": [
        "mcp",
        "--config",
        "/path/to/repo/.agents/outrig/config.toml",
        "--image",
        "coding"
      ],
      "env": {}
    }
  }
}

Zed uses context_servers in its settings:

{
  "context_servers": {
    "outrig": {
      "command": "outrig",
      "args": [
        "mcp",
        "--config",
        "/path/to/repo/.agents/outrig/config.toml",
        "--image",
        "coding"
      ],
      "env": {}
    }
  }
}

Streamable HTTP

Use --listen when you want one long-lived outrig mcp process that multiple MCP clients can connect to:

outrig mcp --listen 127.0.0.1:7331 --image coding

The MCP endpoint is /mcp, so clients should connect to:

http://127.0.0.1:7331/mcp

Loopback TCP is the intended default deployment shape. Binding 0.0.0.0:7331 or any other non-loopback address is allowed, but outrig prints a warning because v1 has no built-in authentication and anything that can reach the port can call the container’s tools. Put an authenticated reverse proxy in front if you expose it beyond the local machine.

For local multi-process access without a TCP port, use a Unix socket:

outrig mcp --listen unix:/tmp/outrig.sock --image coding

Socket filesystem permissions are the access boundary. HTTP clients still use the Streamable HTTP protocol and the /mcp path over that socket.

What Happens, in Order

  1. Locate config. Walks up from the current directory until .agents/outrig/config.toml is found, or fails. The MCP host’s cwd therefore needs to be the repo, or pass --config <path> explicitly.
  2. Resolve image. Uses explicit --image first. Config entries win; an unknown explicit value is treated as a local Podman image ref. Without explicit --image, top-level default-image still names a config block.
  3. Prepare the container. Fresh mode builds, cache-hits, pulls configured image-name refs, or probes raw local refs, then starts podman run -d --rm --name outrig-<sid> .... Attach mode probes the existing container with podman inspect, verifies that it is running, and does not build, start, stop, or remove it.
  4. Start network interception, if enabled. Fresh sessions can write <session_dir>/logs/network.jsonl and filter mode can enforce global policy; attach mode cannot install a new interceptor.
  5. Merge MCP config. Read the image’s org.outrig.mcp label if present, then overlay [images.<name>.mcp] from config by server name. Repo-local build images stamp this merged table into their cache tags on build misses; raw image refs have no repo config block, so their MCP entries come from labels only.
  6. Connect MCP servers. For each merged entry, podman exec -i the configured command and run the MCP initialize handshake.
  7. Build the proxy. outrig advertises one merged tool list to its client, with each tool namespaced <server>__<tool>. See Tool Names below.
  8. Serve MCP. Without --listen, rmcp’s stdio transport reads JSON-RPC frames from the process stdin and writes responses to stdout. With --listen, rmcp’s Streamable HTTP service accepts POST/SSE traffic at /mcp. The proxy dispatches tools/call to the right backing server in both modes.

If anything before step 8 fails, outrig mcp prints the error on stderr and exits non-zero without ever advertising a tool list.

Startup Banner

After the image is ready, the container is running, and all backing MCP servers have answered tools/list, outrig mcp prints one banner to stderr:

[outrig] image-config:  coding
[outrig] image:             outrig:coding-1f3a2b
[outrig] container started: outrig-coding-2026-05-04-a83f
[outrig] mcp fs:    initialized (3 tools)
[outrig] mcp shell: initialized (1 tool)
[outrig] tools available: fs__list_directory, fs__read_file, fs__write_file, shell__exec
[outrig] session id: 20260504T141907-a83f
[outrig] transport: stdio
[outrig] mcp server ready

Everything in that banner is on stderr. The client should treat stdout as protocol bytes only for stdio transport.

Attach mode prints container attached: in the same position.

With --listen, the banner says transport: streamable-http, then prints the bound endpoint before the ready line:

[outrig] transport: streamable-http
[outrig] listen: http://127.0.0.1:7331/mcp
[outrig] mcp server ready

Transport Discipline

By default, outrig mcp serves MCP over stdio. Its stdout is reserved for JSON-RPC messages to the client; all other process output goes somewhere else:

  • startup banner: stderr
  • outrig tracing controlled by OUTRIG_LOG, or RUST_LOG when unset: stderr
  • top-level startup errors: stderr
  • backing server stderr: <session_dir>/logs/<server>.stderr

This split is load-bearing. If a wrapper, shell hook, or debug print writes anything non-JSON to stdout before or during the MCP exchange, the external client may fail the handshake or drop the server.

With --listen, stdout is not the protocol channel, but outrig keeps the same stderr-first discipline so wrapper behavior remains predictable.

Tool Names

Outrig publishes tools under <server>__<tool> names. If the backing fs server advertises a tool named read_file, the external client sees:

fs__read_file

Some MCP clients display that relationship as fs.read_file in their UI. The actual tool name on the wire is still fs__read_file; outrig strips the fs__ prefix and dispatches the call to the original read_file tool on the fs backing server.

See Concepts -> MCP Servers for the collision and sanitization rules.

Lifecycle

outrig mcp has three graceful shutdown triggers:

  • stdio stdin EOF, which usually means the external MCP client disconnected
  • SIGINT, such as Ctrl-C in the terminal that launched the process
  • SIGTERM, such as a supervisor asking the process to stop

All paths cancel the rmcp service, wait for the dispatcher to settle, shut down each backing MCP server, and finalize the session record. Fresh-container mode then stops the container. Attach mode leaves the borrowed container running.

HTTP/SSE mode is daemon-shaped: client disconnects close only that MCP session. The outrig mcp --listen process stays alive until SIGINT, SIGTERM, or attached-container shutdown.

If an attached host session stops the container while outrig mcp --attach is live, the attacher cancels its proxy, shuts down its MCP children, finalizes its session with a non-zero exit, and reports that the attached container stopped.

Sessions and Logs

Every outrig mcp invocation creates a normal outrig session, including attach mode. It appears in outrig ls, its backing-server stderr is readable with outrig logs, and outrig discard removes it like any other session.

Because no agent participates, the in-memory session row has agent_name = None. On disk, new outrig mcp session JSON omits agent_name; older JSON with "agent_name": null still represents the same no-agent state when read.

$ outrig ls
ID                     STARTED              DURATION  IMAGE      EXIT
20260504T141907-a83f   2026-05-04 14:19:07  0m44s     coding     0

$ outrig logs 20260504T141907-a83f fs
[mcp-server-filesystem] starting; root=/workspace
[mcp-server-filesystem] tools/list: 3 tools advertised

See Sessions for session-root resolution, --session-dir, and log inspection.

Future Work

The v0 HTTP surface is intentionally unauthenticated and minimal. Deferred work includes adding built-in auth, exposing a tool-call audit log, proxying MCP prompts/* and resources/*, surfacing backing-server stderr as MCP resources, and paginating tools/list.

See Also

outrig build

outrig build builds (or cache-hits) the image for one or more image-configs, without starting an agent session. It’s useful for:

  • Verifying your Dockerfile builds cleanly before you start an agent.
  • Pre-warming the image so the first outrig run is fast.
  • CI: ensuring the agent’s environment still builds after a dependency bump.

Synopsis

outrig build [--image <name>]
             [--config <path>]
             [--no-cache]
             [--all]
  • --image <name> (default: default-image): build a specific named image-config.
  • --all (default: off): build every image-config defined in the config file.
  • --config <path> (default: walks up from cwd): use a non-default config path.
  • --no-cache (default: off): force rebuild even on cache hit. Passes --no-cache to buildah.

What it does

  1. Loads .agents/outrig/config.toml and validates every image-config in the merged config. Agent/model wiring is not required for image builds.
  2. For each selected image-config:
    • Computes the cache key (blake3 over Dockerfile content, resolved build-args, the OutRig labels derived from [images.<name>.mcp], and the context content hash).
    • If a tag matching that key exists and --no-cache is not set, prints image ready (cache hit) and skips.
    • Otherwise runs a buildah build to a temporary tag:
      buildah build --tag <image-config-name>:outrig-tmp-... \
                    --file <dockerfile> \
                    [user build-args] \
                    <context>
      
      Then it reads any inherited/Dockerfile org.outrig.mcp label, overlays [images.<name>.mcp], and commits the final <image-config-name>:<hash> image with the merged org.outrig.mcp label. The repository is the [images.<name>] block key, so the built image is self-describing in podman images; the <hash> is the content-addressed cache key.

Examples

Build the default:

$ outrig build
[outrig] image-config: coding
[outrig] dockerfile:       .agents/outrig/images/coding/Dockerfile
[outrig] context:          .agents/outrig/images/coding
[outrig] image tag:        coding:8c2a4f7e91d6b5a3
[buildah] STEP 1/6: FROM docker.io/library/node:20-bookworm-slim
...
[outrig] image ready: coding:8c2a4f7e91d6b5a3

Cache hit on the second run:

$ outrig build
[outrig] image ready (cache hit: coding:8c2a4f7e91d6b5a3)

Build a specific image-config:

$ outrig build --image planning
[outrig] image-config: planning
...
[outrig] image ready: planning:b91e3a6d217f4c08

Build every image-config in one go:

$ outrig build --all
[outrig] image-config: coding   -> coding:8c2a4f7e91d6b5a3 (cache hit)
[outrig] image-config: planning -> planning:b91e3a6d217f4c08 (built in 27s)
[outrig] all images ready

Force a rebuild without changing files:

$ outrig build --no-cache

Exit codes

  • 0 – every selected image is built or cache-hit.
  • non-zero – at least one image failed to build. The buildah stderr is reproduced with the [buildah] prefix, ending with the exact failing step.

See also

Sessions

A session is the on-disk record of one outrig run or outrig mcp invocation: when it started and ended, which image-config it used, what image tag, plus per-MCP-server stderr captured to disk. Sessions exist so you can go back and inspect what happened – they are not a staging area for workspace changes (outrig writes to your repo directly; see Workspace).

Where sessions live

Sessions live under a session root directory. Resolution order:

  1. --session-root <path> flag (any session-touching subcommand).
  2. session-root key in repo or global config.
  3. <XDG_DATA_HOME>/outrig/sessions/ (default; typically ~/.local/share/outrig/sessions/).

Set session-root once in ~/.outrig/config.toml to keep sessions somewhere persistent like /var/lib/outrig/sessions/:

session-root = "/var/lib/outrig/sessions"

Layout under the root

Each session is a subdirectory of the root, named by session id (a UTC timestamp plus four random hex digits – sortable, unambiguous across concurrent runs):

~/.local/share/outrig/sessions/
└── 20260501T134412-3f2a/
    ├── session.json              # id, timestamps, container, image, exit status
    └── logs/
        ├── container.log         # buildah/podman transcripts when --verbose is set
        ├── network.jsonl         # network audit/filter records when enabled
        ├── fs.stderr             # MCP "fs" server's captured stderr
        └── shell.stderr          # MCP "shell" server's captured stderr

When you run outrig run --session-dir <path>, outrig writes the session content into <path> directly and creates a symlink at <root>/<sid> -> <path> so outrig ls still finds it:

~/.local/share/outrig/sessions/
├── 20260501T134412-3f2a/        # auto-allocated, real directory
└── 20260501T141907-9b1c -> /tmp/my-debug-run/    # explicit --session-dir, symlink

This means you can launch with a known path and read session.json, logs/, etc. directly without first looking up the auto-generated session id. Useful for scripted runs and quick debugging:

$ outrig run --session-dir /tmp/my-debug-run < prompts.txt
$ cat /tmp/my-debug-run/session.json
$ tail -f /tmp/my-debug-run/logs/shell.stderr

outrig ls

List sessions newest-first:

$ outrig ls
ID                     STARTED              DURATION  IMAGE      EXIT
20260501T141907-9b1c   2026-05-01 14:19:07  0m44s     coding     0   -> /tmp/my-debug-run
20260501T134412-3f2a   2026-05-01 13:44:12  2m18s     coding     0
20260430T091203-44d2   2026-04-30 09:12:03  6m02s     planning   1

EXIT is the outrig process exit code, not the agent’s “did it succeed” signal – there’s no formal way for the agent to declare success or failure. A non-zero exit usually means an LLM API error, a container failure, or a SIGINT after the user gave up. The -> /path suffix marks sessions whose root entry is a symlink (created via outrig run --session-dir).

outrig mcp sessions have no agent. Their in-memory session row has agent_name = None; new on-disk session.json files omit agent_name, and older records with "agent_name": null mean the same thing. outrig ls does not currently show an agent column, so these sessions appear with the same ID / STARTED / DURATION / IMAGE / EXIT columns as outrig run sessions.

Override the root for a single invocation with --session-root:

$ outrig ls --session-root /var/lib/outrig/sessions

outrig logs

Print one server’s captured stderr by session id:

$ outrig logs 20260501T134412-3f2a fs
[mcp-server-filesystem] starting; root=/workspace
[mcp-server-filesystem] tools/list: 3 tools advertised
[mcp-server-filesystem] tools/call: list_directory({"path": "/workspace"})
[mcp-server-filesystem] tools/call: read_file({"path": "/workspace/HELLO.txt"})
[mcp-server-filesystem] EOF on stdin; shutting down

Or read directly from a session directory (skips the id lookup):

$ outrig logs --session-dir /tmp/my-debug-run fs

Omit the server name to get a directory listing of available logs:

$ outrig logs 20260501T134412-3f2a
[outrig] logs for session 20260501T134412-3f2a:
  fs       (1.2 KiB)
  shell    (3.4 KiB)

Follow a still-running session’s log:

$ outrig logs 20260501T141907-9b1c shell --follow
[shell-mcp-command] starting
[shell-mcp-command] exec: cargo check
warning: unused import: `std::collections::HashMap`
   --> src/lib.rs:3:5
    |
3   | use std::collections::HashMap;
...

--follow (-f) tails the file with tail -F-style behavior: continues when the file rotates, returns when EOF is reached and no further writes are expected.

When network audit or filter mode is enabled, logs/network.jsonl contains one Zeek conn.log-style JSON object per outbound connection. It is not selected with the server-name argument because it is not an MCP stderr log; read it directly from the session directory:

$ jq . /tmp/my-debug-run/logs/network.jsonl

The log uses Zeek field names where OutRig has equivalent data: ts is Unix epoch seconds, uid is a per-connection id, id.orig_h / id.orig_p identify the container-side socket, id.resp_h / id.resp_p identify the remote endpoint, proto is the transport, service is the sniffed application (http, ssl, ssh, or -), duration is seconds, and orig_bytes / resp_bytes count bytes sent and received by the container. OutRig-specific metadata is namespaced under outrig.*, including outrig.action, outrig.rule, and outrig.host.

outrig discard

Delete a session’s entire on-disk record (including logs):

$ outrig discard 20260430T091203-44d2 --yes
[outrig] removed ~/.local/share/outrig/sessions/20260430T091203-44d2

For sessions whose root entry is a symlink, discard removes both the real directory at the symlink target and the symlink itself:

$ outrig discard 20260501T141907-9b1c --yes
[outrig] removed /tmp/my-debug-run
[outrig] removed ~/.local/share/outrig/sessions/20260501T141907-9b1c (symlink)

You can also point at a session directory directly:

$ outrig discard --session-dir /tmp/my-debug-run --yes

--yes skips the interactive confirmation. This is destructive (it rm -rf’s the session directory) but only of the session record itself – your repository is untouched. A session that’s still running can’t be discarded; outrig refuses with an error pointing at the running container.

outrig clean

Delete old stopped session records in bulk. Bare outrig clean uses a 30-day retention window, previews the sessions it will remove, then asks once before deleting:

$ outrig clean
[outrig] will remove 2 sessions older than 30d:
  20260401T134412-3f2a  ended 2026-04-01 13:46:30  /tmp/old-a
  20260402T091203-44d2  ended 2026-04-02 09:18:54  /tmp/old-b
Clean 2 sessions? [y/N]:

Use --older-than to pick a different cutoff. Durations are positive integers with s, m, h, or d units:

$ outrig clean --older-than 7d
$ outrig clean --older-than 12h

Use --yes (-y) to skip the prompt:

$ outrig clean --older-than 30d --yes

outrig clean uses the same deletion semantics as outrig discard: auto-allocated sessions remove the session directory, while sessions created with --session-dir remove both the symlink target and the symlink under the session root. Running sessions are skipped so cleanup doesn’t race a live outrig run or outrig mcp writer.

What sessions don’t track

  • Workspace changes. outrig uses a direct bind-mount; mutations land on your host filesystem in real time. Use git diff, git status, etc. for review. There is no per-session changeset on disk.
  • Conversation history. v0 doesn’t persist the LLM conversation. If you need a transcript, redirect stdin/stdout (outrig run < prompts.txt > replies.txt). --verbose captures buildah/podman traces in container.log; it is not a conversation transcript.
  • API keys. The bearer token used for the LLM call is never written to disk and never appears in tracing output.

TODO: Incomplete – opt-in transcript capture (per-turn JSON of user/assistant/tool messages) is deferred.

Concurrent sessions

You can run multiple outrig run invocations against the same repository simultaneously. Each gets its own container, its own session id, its own logs directory. They share the bind-mounted workspace, so file writes in one session are visible to the other immediately – be aware if you’re running two agents on overlapping work.

See also

Recipes

A handful of common patterns. None of them require anything outside the documented commands and config – they’re just compositions worth knowing.

Recipe: scripted single-prompt runs

Pipe a single prompt in, capture only the assistant’s reply:

$ echo "list every Cargo.toml in this repo and summarise what it builds" \
    | outrig run > summary.txt 2> outrig.stderr
$ cat summary.txt
This repo is a Cargo workspace with one crate: outrig (a Rust binary). The
single Cargo.toml at the root declares dependencies on clap, tokio, ...

Because outrig keeps tool-call traces, container-startup messages, and slash-command output strictly on stderr, redirecting stdout gives you only model output.

This is the pattern for embedding outrig in larger shell pipelines (CI checks, batch generation, summarization jobs).

Recipe: separate image-configs for different tasks

A coding config has the full toolchain. A planning config has only research-style MCPs and runs against a cheaper model. Use distinct agents (each pointing at its own image-config) and pick with --agent:

default-image = "coding"
default-agent     = "coding"

[agents.coding]
model     = "fast"
image     = "coding"
preamble  = "You are a careful coding assistant."

[agents.planning]
model     = "fast"
image     = "planning"
preamble  = "You are a research planner. Read, summarise, do not execute."

[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"
context    = ".agents/outrig/images/coding"

  [images.coding.mcp]
  fs    = { command = ["mcp-server-filesystem", "/workspace"] }
  shell = ["bash", "-lc", "exec shell-mcp-command"]

[images.planning]
dockerfile = ".agents/outrig/images/planning/Dockerfile"
context    = ".agents/outrig/images/planning"

  [images.planning.mcp]
  fs       = { command = ["mcp-server-filesystem", "/workspace"] }
  research = { command = ["mcp-research"] }
$ outrig run                  # default-agent = "coding"
$ outrig run --agent planning # different agent, different image, different MCPs

Because each image-config caches independently, switching is fast after the first build of each.

Recipe: pre-warming images in CI

Catch a broken Dockerfile or dependency bump before anyone tries to use the agent:

# .github/workflows/outrig-images.yml (sketch)
- run: cargo install outrig-cli
- run: outrig build --all

outrig build --all rebuilds every image-config, returns non-zero on the first failure, and prints the failing buildah step.

Recipe: capturing what the agent did

Two complementary approaches.

Tool-call traces via --verbose:

$ outrig run --verbose 2> trace.log
> add unit tests for parse_config
...
> ^D
$ less trace.log

trace.log contains every buildah/podman invocation, every MCP tool call with its arguments, and every assistant turn delineator.

git as the source of truth for filesystem changes:

$ git checkout -b agent/test-coverage
$ outrig run
> add unit tests for parse_config
...
> ^D
$ git status
$ git diff
$ git log --oneline main..HEAD     # what the agent committed (if anything)

Combined: --verbose for what the agent decided, git for what landed on disk.

Recipe: testing a config from outside the repo

If you’re iterating on the config and don’t want to cd every time:

$ outrig run --config ~/work/some-repo/.agents/outrig/config.toml

--config overrides the default upward-walk. Pair it with --session-dir to write this run’s session into a known directory you can read directly:

$ outrig run \
    --config ~/work/some-repo/.agents/outrig/config.toml \
    --session-dir /tmp/outrig-experiment

$ cat /tmp/outrig-experiment/session.json   # no id lookup needed
$ tail -f /tmp/outrig-experiment/logs/shell.stderr

--session-dir writes the session content under /tmp/outrig-experiment/ and adds a symlink <session-root>/<sid> -> /tmp/outrig-experiment/ so the run still appears in outrig ls. To move the root (where new auto-generated sessions live) for the duration of one command, use --session-root instead:

$ outrig run --session-root /var/lib/outrig/sessions

Recipe: sharing a config across repositories

The config file is just TOML – symlink or copy it. A common pattern is a single template repo with .agents/outrig/config.toml and a Dockerfile, then git subtree add (or simply copy) into per-project repos. You can keep the Dockerfile centralized too if it doesn’t need per-repo customization.

TODO: Incomplete – outrig has no built-in mechanism for sharing configs; this is plain filesystem composition.

Recipe: limiting the agent’s filesystem reach

Mount a subset of the repo by setting [workspace] host-path to something narrower than .:

[workspace]
host-path      = "src"
container-path = "/workspace"

The agent now sees only src/. It can’t read Cargo.toml, .git, target/, etc. Useful when you want a tightly scoped sandbox for a refactor that shouldn’t need to touch build configuration.

See also

Reference

The reference section is meant for lookup, not for reading start-to-finish. If you’ve read Usage and want the precise list of flags or config keys, you’re in the right place.

  • CLI – every subcommand and every flag.
  • Config – every key in .agents/outrig/config.toml, with types and defaults.

For narrative explanations of why a key exists or how a subcommand fits into a workflow, see Concepts and Usage.

CLI Reference

Synopsis

outrig <SUBCOMMAND> [FLAGS] [ARGS]
outrig --version
outrig --help

Global flags

These are accepted by every subcommand.

FlagDescription
--config <path>Path to repo config.toml. Default: walk up from cwd.
--global-config <path>Path to global config. Default: ~/.outrig/config.toml.
--session-root <path>Root directory containing sessions. Overrides config + XDG.
-v, --verbosePrint buildah/podman transcripts; repeat for trace logs.
--helpPrint subcommand help.

--config resolves in this order: this flag (used verbatim – no walk-up, no existence check), then a walk up from cwd looking for .agents/outrig/config.toml. The walk stops at the filesystem root; a .agents/ directory without an outrig/config.toml inside does not terminate the walk – outrig keeps looking in parents.

--global-config resolves in this order: this flag, then <XDG_CONFIG_HOME>/outrig/config.toml when XDG_CONFIG_HOME is set, then ~/.outrig/config.toml (the outrig-specific fallback – note this is ~/.outrig/, not ~/.config/outrig/).

--session-root resolves in this order: this flag, then session-root in the repo or global config, then <XDG_DATA_HOME>/outrig/sessions/.

--verbose adds buildah/podman command transcripts to stderr and to <session_dir>/logs/container.log for outrig run / outrig mcp. Repeat it (-vv) to also enable trace-level logs from outrig’s own modules for that invocation. It does not change container, MCP, or agent behavior. Normal startup progress is printed to stderr without --verbose.

Subcommands

outrig init

Idempotent end-to-end setup orchestrator. Runs outrig config init if the global config is missing, writes .agents/outrig/config.toml if it doesn’t exist, then offers to call outrig image add in a loop.

outrig init [--force]
FlagDefaultDescription
--forceoffOverwrite existing files. Propagates to config init and image add.

See Usage -> outrig init.

outrig config init

Interactively write the global config (~/.outrig/config.toml): provider styles, models, and default-model. The first subcommand of the outrig config group; future subcommands (config get, config set, config list) are deferred.

outrig config init [--force]
FlagDefaultDescription
--forceoffOverwrite an existing global config; otherwise refuses to clobber.

See Usage -> outrig config.

outrig design prompt

Print a self-contained prompt for AI-assisted image design, or print an MCP setup snippet for tools that can attach outrig mcp self.

outrig design prompt
outrig design prompt --standalone
outrig design prompt --print-mcp-config <tool>
FlagDefaultDescription
--standaloneoffPrint a prompt for a standalone image project.
--print-mcp-config <tool>nonePrint a setup snippet; wins over --standalone.

The valid <tool> names are claude-code, claude-desktop, codex, and cursor.

See Usage -> AI-assisted design.

outrig image add

Interactively scaffold an image-config: writes a Dockerfile under .agents/outrig/images/<name>/Dockerfile and adds the matching [images.<name>] and [images.<name>.mcp] blocks to the repo config. Use outrig image init instead for a standalone image project; image ls / image rm are deferred.

outrig image add [<name>]
                 [--force]
Argument / flagDefaultDescription
<name>promptedImage-config name.
--forceoffOverwrite existing files for this name.

See Usage -> outrig image.

outrig image init

Noninteractively scaffold a standalone image project: writes a Dockerfile, an authoring image.toml, and a README.md into a target directory. The directory name becomes the image ref; other repos consume the built image via image-name.

outrig image init [<dir>]
                  [--force]
Argument / flagDefaultDescription
<dir>current dirProject directory; its name becomes the image ref.
--forceoffOverwrite the generated files if they already exist.

See Usage -> outrig image.

outrig image build

Build a standalone image project (the output of outrig image init) with buildah, tag it by its image.toml [image].ref, stamp its config into OCI labels, and validate the result: the built image must carry a valid org.outrig.mcp label, and – unless --no-test – every declared MCP server must start and answer tools/list.

outrig image build [<dir>]
                   [--tag <ref>]
                   [--no-test]
                   [--no-cache]
Argument / flagDefaultDescription
<dir>current dirProject directory holding image.toml.
--tag <ref>[image].refTag the output as <ref>; does not rewrite image.toml.
--no-testoffSkip the live MCP test; still validates image.toml.
--no-cacheoffForce a clean build. Passes --no-cache to buildah.

See Usage -> outrig image.

outrig image inspect

Inspect an image’s OutRig OCI labels without pulling it, creating a container, or starting any MCP server. By default this reads the local image store; --remote reads registry metadata with skopeo inspect.

outrig image inspect [--remote] <ref>
ArgumentDescription
<ref>Image ref to inspect. Never pulled.
--remoteRead registry metadata with skopeo inspect.

See Usage -> outrig image.

outrig run

Start an interactive agent session.

outrig run [--agent <name>]
           [--image <name-or-local-ref>]
           [--config <path>]
           [--device <cpu|cuda|cuda:N|metal>]
           [--env <KEY=VALUE>]
           [--global-config <path>]
           [--max-tool-calls <n>]
           [--max-tool-result-bytes <n>]
           [--model <name>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--session-root <path>]
           [--volume <host:container[:ro|rw]>]
           [--verbose]
  • --agent <name> (default: default-agent): selects an [agents.<name>] block.
  • --image <name-or-local-ref> (default: from agent or default-image): image-config to launch. If an explicit value does not match config, it is treated as a local Podman image ref and is not pulled.
  • --env <KEY=VALUE> (repeatable): add or override env vars for MCP servers. KEY=VALUE applies to every server; SERVER:KEY=VALUE targets a single server by name. Values support the ${VAR} host-env-reference syntax described in config.md#mcp-env-value-syntax. Within a scope, last wins on duplicate keys. Precedence per key: config-file env < global --env < per-server --env.
  • --device <cpu|cuda|cuda:N|metal> (default: mistralrs model device, else cpu): override the in-process mistralrs model device for this run. Rejected for OpenAI-style models.
  • --max-tool-calls <n> (default: resolved tool-call-max, else 50): per-turn tool-call max.
  • --max-tool-result-bytes <n> (default: resolved tool-result-max, else 262144): per-tool-result byte max.
  • --model <name> (default: agent’s model, else default-model): configured [models.<name>] entry to use for this run. This is not a raw provider model identifier.
  • --network <default|audit|filter> (default: config [network].mode, else default): choose Podman’s default networking, network audit logging, or global network filtering for this session.
  • --session-dir <path> (default: <session-root>/<sid>): specific directory for this run.
  • --volume <host:container[:ro|rw]> (repeatable): bind an extra host directory into the container, beyond the default workspace mount. Read-only unless :rw; host dir must exist.
  • -v, --verbose (default: off): print container lifecycle traces.

When --session-dir is given, outrig writes this run’s session.json and logs/ directly under <path>, and additionally creates a symlink <session-root>/<sid> -> <path> so outrig ls/logs/discard/clean still find it. When omitted, outrig auto-generates a session id and writes to <session-root>/<sid>/ directly.

Reads the global and repo configs, resolves agent -> model -> provider, builds the image (cache-hit if possible), starts the container, attaches every MCP server, opens the REPL. Exits when stdin reaches EOF, when the user types /quit, or after a second Ctrl-C.

With no repo config found and no --config, run and mcp use the current directory as the workspace root and take all config from the global file; run then needs its agent from the global config and an explicit --image. build still requires a repo config.

See Usage -> outrig run for REPL details.

outrig mcp

Serve the selected image’s backing MCP servers as one MCP server over stdio.

outrig mcp [--image <name-or-local-ref>]
           [--attach <session-id-or-container-name>]
           [--listen <addr>]
           [--env <KEY=VALUE>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--config <path>]
           [--global-config <path>]
           [--session-root <path>]
           [--volume <host:container[:ro|rw]>]
           [--verbose]

outrig mcp show-merged [--image <name-or-local-ref>]
                       [--attach <session-id-or-container-name>]
                       [--session-dir <path>]
                       [--config <path>]
                       [--global-config <path>]
                       [--session-root <path>]
                       [--verbose]

outrig mcp self
FlagDefaultDescription
--image <name-or-local-ref>default-imageImage-config, or explicit local Podman image ref.
--attach <id-or-name>offReuse an existing container.
--listen <addr>offServe Streamable HTTP at /mcp.
--env <KEY=VALUE>Override MCP env; repeatable. As run.
`–network <defaultauditfilter>`
--session-dir <path><session-root>/<sid> (auto)Specific directory for this server.
--volume <spec>Extra bind mount; not with –attach.
-v, --verboseoffPrint container lifecycle traces.

There is no --agent flag. outrig mcp does not resolve default-agent, does not let agent.image participate in image-config selection, and does not read provider API keys. Image selection is explicit --image, then top-level default-image, then an error. Config entries win; an unknown explicit --image is treated as a local Podman image ref. default-image remains config-only.

Like outrig run, mcp runs config-less when no .agents/outrig/config.toml is found (and no --config): the current directory becomes the workspace root and config comes from the global file. With no agent to resolve, --image <local-ref> is enough.

With --attach, the value is resolved first as an exact session id under the resolved session root. A session match supplies the podman container name and default image-config or raw image ref. If there is no session match, the value is treated as a podman container name and --image <name-or-local-ref> is required.

Startup builds or cache-hits the image and starts the container unless --attach is set. Attach mode validates the existing container with podman inspect and borrows it without stopping or removing it during teardown. Both modes initialize every entry in the merged MCP table, list their tools, print a banner to stderr, and then speak MCP JSON-RPC on stdout/stdin. The merged table is the image’s org.outrig.mcp label plus [images.<name>.mcp] overrides. Build-from-Dockerfile repo images stamp that merged table into their cache tag on build misses, so outrig image inspect <name>:<hash> can report the same declared servers without serving MCP. All non-protocol output stays off stdout.

When --listen <addr> is set, outrig mcp serves Streamable HTTP instead of stdio. TCP addresses are socket addresses such as 127.0.0.1:7331 or 0.0.0.0:7331; Unix sockets use unix:/tmp/outrig.sock. The HTTP MCP endpoint is always /mcp. Loopback TCP is local-only by default. Non-loopback TCP binds are allowed but print a warning because this v1 surface has no built-in auth; use an authenticated reverse proxy before exposing it broadly. Each HTTP MCP client gets its own rmcp session backed by the same shared outrig proxy and backing MCP processes.

--network audit and --network filter are supported only for fresh-container outrig mcp sessions. Attach mode cannot retrofit a borrowed container with a new interceptor.

outrig mcp show-merged uses the same image-config selection and setup path, but exits after printing the effective [mcp] table to stdout. It is for debugging image-label declarations and repo-local overrides, not for serving MCP JSON-RPC.

outrig mcp self serves host-side self-description tools over stdio. It does not start a container or require a repo config. Use it from an external MCP-capable AI tool when the built-in image templates do not fit.

Trigger or failureExit
Stdio client closes stdin after successful startup0
SIGINT or SIGTERM after successful startup0
Config, image, container, or MCP startup failure1
Bad flags or missing required args2

Environment variables used by outrig mcp:

VariableEffect
OUTRIG_LOGPreferred tracing-subscriber filter.
RUST_LOGFallback tracing filter when OUTRIG_LOG is unset.
XDG_DATA_HOMEDefault base for session-root if not set in config.
XDG_CONFIG_HOMEGlobal config is checked before ~/.outrig/config.toml.

See Usage -> outrig mcp for client configuration and stdio details.

outrig build

Build (or cache-hit) one or more image-config images, without starting an agent.

outrig build [--image <name>]
             [--all]
             [--no-cache]
             [--config <path>]
  • --image <name> (default: default-image): build a specific named image-config.
  • --all (default: off): build every image-config. Mutually exclusive with --image.
  • --no-cache (default: off): force rebuild even on cache hit. Passes --no-cache to buildah.

See Usage -> outrig build.

outrig ls

List sessions newest-first under the session root.

outrig ls [--session-root <path>]

Output columns: ID, STARTED, DURATION, CONTAINER, EXIT. Symlinked sessions (created via outrig run --session-dir) display the symlink target as a hint.

See Usage -> Sessions.

outrig logs

Print or follow a session’s MCP-server stderr.

outrig logs [<session>] [<server>]
            [--follow]
            [--session-dir <path>]
            [--session-root <path>]
Argument / flagDefaultDescription
<session>omit with --session-dirSession id; resolved under session root.
<server>list available logsServer name from [images.<name>.mcp].
--follow, -foffTail the log; continue reading new lines.
--session-dir <path>Read directly from this session dir.

<session> and --session-dir are mutually exclusive: pass one or the other. Substring match on <session> is allowed if unambiguous.

outrig discard

Delete a session’s on-disk record (logs and metadata).

outrig discard [<session>] [--yes]
                           [--session-dir <path>]
                           [--session-root <path>]
Argument / flagDefaultDescription
<session>omit with --session-dirSession id; resolved under session root.
--yes, -yoffSkip the interactive confirmation.
--session-dir <path>Discard exactly this session dir.

<session> and --session-dir are mutually exclusive. If the session was created via outrig run --session-dir <path> (i.e. lives at a user-chosen path with a symlink in the root), discard removes the real directory and the symlink. Refuses if the session’s container is still running. Discards the session directory only – your repository is untouched.

outrig clean

Delete old stopped session records in bulk.

outrig clean [--older-than <duration>]
             [--yes]
             [--session-root <path>]
Argument / flagDefaultDescription
--older-than <duration>30dRemove sessions older than cutoff.
--yes, -yoffSkip the interactive confirmation.

Durations are positive integers with s, m, h, or d units, for example 12h or 7d. The command previews matching sessions and asks once before deleting unless --yes is set. Running sessions are skipped. Sessions created with --session-dir remove both the symlink target and the symlink under the session root.

Exit codes

CodeMeaning
0Success.
1Generic failure (config, image build, LLM API error, etc.).
2Misuse (bad flags, missing required args). clap prints the usage line.
130Interrupted by SIGINT before subcommand-specific handling.

Environment variables

  • [providers.<name>].api-key references via ${VAR}: provider API key.
  • OUTRIG_LOG: preferred tracing-subscriber filter, e.g. OUTRIG_LOG=debug.
  • RUST_LOG: fallback tracing filter when OUTRIG_LOG is unset.
  • XDG_DATA_HOME: default base for session-root if not set in config.
  • XDG_CONFIG_HOME: global config is checked here before ~/.outrig/config.toml.

See also

Config Reference

outrig reads two TOML files:

  • Global config – user/machine-level. Default location: ~/.outrig/config.toml (or <XDG_CONFIG_HOME>/outrig/config.toml if XDG_CONFIG_HOME is set). Holds [providers.<name>] and (typically) [models.<name>] since those reference API keys and model identifiers that belong to the user, not to any one repo.
  • Repo config at .agents/outrig/config.toml – repository-level, committed to source control. Holds [workspace], [images.<name>], [agents.<name>], and any repo-specific providers or models.

Both files use the same schema. Names declared in either are visible everywhere; if a name appears in both files, the repo entry wins. Outrig keys are kebab-case; only inner-map keys whose values map to environment variables (Dockerfile build-args, MCP env blocks) keep their as-written form. Unknown keys are an error – outrig validates with deny_unknown_fields.

Top level

# repo config (.agents/outrig/config.toml):
default-image = "coding"
default-agent     = "coding"

# global config (~/.outrig/config.toml):
default-model     = "fast"
session-root      = "/var/lib/outrig/sessions"        # optional; defaults to XDG data dir
model-cache-root  = "/var/cache/outrig/models"        # optional; defaults to XDG cache dir
tool-call-max     = 100                               # optional; defaults to 50
tool-result-max   = 262144                            # optional; defaults to 256 KiB

[network]
mode = "default"                                      # optional: default, audit, or filter
default = "deny"                                      # optional for filter mode
allow = ["github.com:443", "*.npmjs.org"]             # optional; global only
deny  = ["*:22"]                                      # optional; global only
KeyTypeRequiredWhereDescription
default-imagestringfor outrig runrepoDefault --image.
default-agentstringfor outrig runrepoDefault --agent.
default-modelstringif agent omits modelglobalFallback model name.
session-rootpathnoglobalSessions root dir.
model-cache-rootpathnoglobalGGUF download cache dir.
tool-call-maxintegernoglobalPer-turn tool-call max.
tool-result-maxintegernoglobalPer-tool-result byte max.
network.modestringnoeitherNetwork mode.
network.defaultstringnoglobalFilter fallback action.
network.allowarraynoglobalFilter allow entries.
network.denyarraynoglobalFilter deny entries.

default-image and default-agent belong in the repo config – image-configs and agents are project-scoped. default-model, session-root, model-cache-root, and tool-call-max belong in the global config since they’re user/machine-level. tool-result-max usually belongs there too, although repo or agent config can tighten it for a noisy project. [network].mode can live in either file; when both set it, the repo value wins for that repo. Network policy keys (default, allow, and deny) are global-only because they describe the machine’s egress policy, not a project preference. Each may also appear in the other file; repo entries override global by name.

session-root defaults to <XDG_DATA_HOME>/outrig/sessions/ (typically ~/.local/share/outrig/sessions/). The CLI flag --session-root <path> overrides both the config value and the default; --session-dir <path> (on outrig run/logs/discard) instead points at one specific session directory. See Sessions.

model-cache-root defaults to <XDG_CACHE_HOME>/outrig/models/ (typically ~/.cache/outrig/models/). It only matters for style = "mistralrs" models configured with model-id – that’s where the auto-downloaded GGUFs land. See Concepts -> In-process LLMs.

tool-call-max is the default maximum number of tool calls in one user turn. The compiled-in default is 50; config may set any value from 1 through 2000. [agents.<name>].tool-call-max overrides the top-level value for one agent, and outrig run --max-tool-calls <n> overrides both for one invocation.

tool-result-max is the default maximum byte size for one MCP tool result before it is added to the LLM-visible conversation history. The compiled-in default is 262144 bytes (256 KiB); config may set any value from 1024 through 16777216 bytes. [agents.<name>].tool-result-max overrides the top-level value for one agent, and outrig run --max-tool-result-bytes <n> overrides both for one invocation. Results larger than the max are truncated at a UTF-8 boundary and end with an [outrig: tool result truncated] marker that reports the original size and max.

[network]

Network interception is disabled by default:

[network]
mode = "default"

Accepted modes:

  • default: use Podman’s configured default networking, do not install the interceptor, and do not write logs/network.jsonl.
  • audit: allow all outbound session-container traffic, but write Zeek conn.log-style records to <session_dir>/logs/network.jsonl.
  • filter: install the same interceptor as audit mode, write the same audit log, and enforce global allow/deny policy before opening upstream TCP connections.

Audit and filter mode require host nft and nsenter plus permission to enter the rootless podman container’s user/network namespaces. It rewrites the session container’s /etc/resolv.conf to send DNS to the per-session in-namespace DNS listener, installs nftables redirection for outbound TCP and UDP/53, and removes the nftables table during teardown. If either mode is requested and setup fails, the session fails before MCP servers launch.

Filter policy lives in the global config only:

[network]
mode    = "filter"
default = "deny"              # optional; absent means "deny" in filter mode
allow   = ["github.com:443", "*.npmjs.org", "10.0.0.0/8"]
deny    = ["*:22", { host = "169.254.169.254", port = 80 }]

allow and deny entries can be compact strings or inline tables. The string "host" maps to { host = "host" }; "host:443" maps to { host = "host", port = 443 }; "*:22" maps to { host = "*", port = 22 }; "[2001:db8::1]:443" maps to an IPv6 host plus port; and CIDRs such as "10.0.0.0/8" or "2001:db8::/32" match IP destinations. Inline tables use { host = "...", port = 443 }, with port optional.

Filter evaluation checks deny entries first, then allow entries, then default. Denied connections are closed immediately and still write an audit record with outrig.action = "deny", outrig.rule, and zero byte counts. mode = "filter" requires at least one allow or deny entry, even when default = "allow".

outrig run --network default|audit|filter and outrig mcp --network default|audit|filter override this setting for one fresh session. --network audit and --network filter are rejected with outrig mcp --attach because borrowed containers are not retrofitted with a new interceptor.

[providers.<name>]

A provider tells outrig how to reach a model – either a remote HTTPS endpoint that speaks a known wire format, or a local in-process backend. Multiple providers in either file. Repo entries with the same name override globals. The accepted style values are "openai" and "mistralrs". Which other fields are valid depends on style.

style = "openai"

[providers.openai]
style    = "openai"
base-url = "https://api.openai.com/v1"
api-key  = "${OPENAI_API_KEY}"

[providers.local-ollama]
style    = "openai"
base-url = "http://localhost:11434/v1"
api-key  = "${OLLAMA_API_KEY}"
KeyTypeRequiredDefaultDescription
stylestringyesMust be "openai" for this row.
base-urlstring (URL)yesHTTPS endpoint for the provider.
api-keystringyesEnv-var reference, see below.
request-timeout-secsintegerno600HTTP timeout for LLM calls.

Each LLM request that fails with a transient error – a timeout, a dropped connection, or an HTTP 408/429/5xx – is retried a few times with exponential backoff before the turn gives up. request-timeout-secs bounds each individual attempt, and defaults high enough not to cut off long reasoning completions.

style = "mistralrs"

In-process LLM backed by the mistralrs crate. No HTTP, no API key. The provider table is bare – just the style tag. Each set of weights is its own [models.<name>] row referencing this provider, so a single mistralrs provider can back many models. See Concepts -> In-process LLMs.

[providers.local]
style = "mistralrs"
KeyTypeRequiredDefaultDescription
stylestringyesMust be "mistralrs" for this row.

base-url and api-key are not allowed on style = "mistralrs". The model-specific fields (model-id, model-path, model-file, revision, context-length, device) live on [models.<name>] – see the mistralrs models subsection.

Always parses, even without --features local-llm

outrig always recognizes style = "mistralrs" for parsing and cross-reference validation, regardless of whether the binary was built with --features local-llm. The build-time feature gates only the use of the provider: trying to resolve an agent that points at a mistralrs provider on a non-feature build fails at run time, with a message that names the missing flag.

The reason is portability – a checked-in .agents/outrig/config.toml can declare both remote and in-process providers, and the same config works for teammates whether or not they built with the feature on.

api-key syntax

api-key must use the env-var-substitution form "${VAR_NAME}" – exactly that, nothing else. outrig refuses any other value, including a plain string that happens to look like an API key. This keeps actual key material out of config files unconditionally:

api-key = "${OPENAI_API_KEY}"   # OK -- outrig reads $OPENAI_API_KEY at run time
api-key = "sk-..."              # ERROR -- looks like a literal key, refused
api-key = "$OPENAI_API_KEY"     # ERROR -- braces required
api-key = "OPENAI_API_KEY"      # ERROR -- ${...} required

The variable name must match ^[A-Z_][A-Z0-9_]*$. If the named environment variable is unset when outrig needs the key, outrig fails with a pointed error.

See Concepts -> LLM Providers.

[models.<name>]

A model points at a provider and supplies whatever that provider needs to identify the weights or wire-format model name. The required fields depend on the provider’s style.

openai-style models

[models.fast]
provider   = "openai"
identifier = "gpt-4o-mini"

[models.smart]
provider   = "openai"
identifier = "gpt-4o"
KeyTypeRequiredDefaultDescription
providerstringyesName of an entry in [providers.<name>].
identifierstringyesModel id passed to the provider API.

mistralrs models

For an in-process style = "mistralrs" provider, the model row carries the weight spec. Either model-id (HuggingFace auto-download) or model-path (local GGUF file) – exactly one. identifier is not allowed on mistralrs models – the weights are the model.

# HuggingFace auto-download:
[models.phi3-fast]
provider   = "local"
model-id   = "microsoft/Phi-3-mini-4k-instruct-gguf"
model-file = "Phi-3-mini-4k-instruct-q4.gguf"
# revision       = "main"   # optional git ref on the HF repo
# context-length = 4096     # optional override
# device         = "cuda"  # optional; defaults to "cpu"

# Multi-shard quantization (one quant split across files):
[models.llama-70b]
provider   = "local"
model-id   = "MaziyarPanahi/Meta-Llama-3-70B-Instruct-GGUF"
model-file = [
    "Meta-Llama-3-70B-Instruct.Q4_K_M-00001-of-00002.gguf",
    "Meta-Llama-3-70B-Instruct.Q4_K_M-00002-of-00002.gguf",
]

# Local GGUF file:
[models.llama-local]
provider   = "local"
model-path = "/var/cache/outrig/models/llama-3-8b-instruct.q4.gguf"
# device     = "metal" # optional; defaults to "cpu"
KeyTypeRequiredDefaultDescription
providerstringyesName of a style = "mistralrs" provider.
model-idstringone of*HF repo id, e.g. microsoft/Phi-3-mini-....
model-pathpathone of*Local path to a GGUF file.
model-filestr/arrwith idGGUF filename(s) inside the HF repo.
revisionstringno"main"HF git ref to pin. With model-id.
context-lengthintegernomodelOverride the model’s default context window.
devicestringno"cpu"One of cpu, cuda, cuda:N, metal.

* Exactly one of model-id / model-path must be set; setting both, or neither, is an error.

device = "cuda" and device = "cuda:N" require a binary built with --features "local-llm cuda"; device = "metal" requires --features "local-llm metal". The feature check happens when an agent resolves the model. Enabling cuda or metal without local-llm emits a build warning and has no effect. outrig does not fall back to CPU if the requested backend is unavailable. With CUDA, cuda:N selects the base device for mistralrs’s automatic mapper; it is not an exclusive single-device sharding directive. outrig run --device <device> overrides this field for one run without editing config.

Metal is only usable on macOS targets. Non-macOS builds can compile with the metal feature for feature-matrix coverage, but trying to instantiate a Metal device fails with a platform error.

[agents.<name>]

An agent is the runnable unit: a model plus a system prompt, optionally bound to an image-config so outrig run --agent <name> knows which sandbox to use.

[agents.coding]
# model omitted -> falls back to top-level default-model
image = "coding"
preamble  = "You are a careful coding assistant. Repo is at /workspace."
temperature = 0.2
max-tokens  = 4096
tool-call-max = 300
tool-result-max = 1048576

[agents.review]
model    = "smart"        # explicit override of default-model
preamble = "You are a meticulous code reviewer..."
  • model (string, optional, default: default-model): name of an entry in [models.<name>].
  • preamble (string, optional, default: minimal default): system prompt for this agent.
  • image (string, optional, default: default-image): default image-config to launch.
  • temperature (float, optional, default: provider default): sampling temperature.
  • max-tokens (integer, optional, default: provider default): output token max per turn.
  • tool-call-max (integer, optional, default: top-level value or 50): tool calls per turn.
  • tool-result-max (integer, optional, default: top-level value or 262144): bytes per result.

If model is omitted, outrig falls back to the top-level default-model; an error if neither is set, except outrig run --model <name> may supply the selected agent’s model for that run. When outrig run --agent <a> runs, the chosen image-config is --image if given, otherwise agents.<a>.image if set, otherwise default-image. tool-call-max is per turn, not per session; follow-up prompts start a fresh count. tool-result-max is per result and applies equally to successful MCP results and MCP error messages.

[workspace]

[workspace]
host-path      = "."
container-path = "/workspace"

[[workspace.mounts]]
host-path      = "../shared-docs"
container-path = "/resources/shared-docs"

[[workspace.mounts]]
host-path      = "/var/tmp/outrig-cache"
container-path = "/resources/cache"
access         = "read-write"
  • host-path (path, optional, default: "."): primary workspace host path, relative to the repo root.
  • container-path (path, optional, default: "/workspace"): where the primary workspace is mounted in the container.
  • workspace.mounts (array, optional, default: []): extra directory bind-mounts.
  • mounts[*].host-path (path, required): host directory to mount. Relative paths resolve against the repo root.
  • mounts[*].container-path (path, required): absolute in-container mount point.
  • mounts[*].access (string, optional, default: "read-only"): either "read-only" or "read-write".

The primary workspace bind-mount is always read-write and uses --userns=keep-id so files written inside the container appear with your host UID/GID. Extra mounts default to read-only; set access = "read-write" only for directories the agent should be able to modify. See Concepts -> Workspace.

[images.<name>]

You declare one or more image-configs. The selected one becomes the agent’s environment. Each block takes exactly one of two shapes:

Build-from-Dockerfile (existing form)

[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"
context    = ".agents/outrig/images/coding"
build-args = { NODE_VERSION = "20" }

For a build-from-Dockerfile image, the block name becomes the built image’s repository (the image is tagged <name>:<content-hash>). The content hash includes Dockerfile/context content, resolved build args, and the OutRig labels derived from [images.<name>.mcp], so MCP config changes produce a new inspectable cache tag. Use a repo-specific, lowercase name (e.g. outrig-standard, not standard) so podman images makes clear which repo it came from. The name must be a valid container image repository component – see the validation rules below.

  • dockerfile (path, required*): path to the Dockerfile, relative to the repo root.
  • context (path, required*): path to the build context, relative to the repo root.
  • build-args (table str->str, optional, default: {}): extra Dockerfile ARGs. Keys are ARG names. Values are either literal strings or ${VAR} references resolved from the host environment at outrig build time; see the MCP env value syntax below.

Use-existing-image (new form)

[images.scratch]
image-name = "docker.io/library/ubuntu:24.04"
  • image-name (string, required*): image reference passed to podman pull / podman run. Accepts any ref form podman supports: name:tag, registry/name:tag, name@sha256:....

* Exactly one of the two shapes must be set. Setting image-name alongside dockerfile, context, or build-args is an error. Setting neither is also an error.

Notes:

  • outrig image add writes its output under .agents/outrig/images/<name>/. You can put Dockerfiles anywhere you want by editing these paths; the .agents/outrig/images/ default just keeps outrig-specific files together.
  • Inner keys of build-args are user-defined Dockerfile ARG names; they’re left as written since they map to env-var-style identifiers.
  • outrig does not inject UID/GID build-args. Host UID/GID are mapped to the container at run time, not baked into the image. See Concepts -> Workspace.

[images.<name>.security]

Optional runtime security controls for the selected container:

[images.coding.security]
capability-profile = "no-net-raw"
cap-drop = ["MKNOD", "SETFCAP"]
cap-add  = ["NET_BIND_SERVICE"]
  • capability-profile (string, optional, default: "default"): named Linux capability profile. Accepted values are:
    • "default": preserve podman’s default capability set and emit no capability flags unless cap-drop or cap-add is set.
    • "no-net-raw": emit --cap-drop=NET_RAW.
    • "drop-all": emit --cap-drop=ALL.
  • cap-drop (array, optional, default: []): extra Linux capabilities to drop.
  • cap-add (array, optional, default: []): Linux capabilities to add after profile and explicit drops are rendered.

Capability names may be written as NET_RAW or CAP_NET_RAW; outrig normalizes to the podman form without the CAP_ prefix. The existing --security-opt=no-new-privileges setting is always applied. This section does not configure seccomp, AppArmor, SELinux, read-only roots, mount policy, or network egress filtering.

[images.<name>.mcp]

Map of MCP server entries, keyed on server name. Each entry is one of two shapes via a serde-untagged dispatch:

[images.coding.mcp]
# Short form -- array of strings, becomes { command = [...] }
shell = ["bash", "-lc", "exec shell-mcp-command"]

# Full form -- table with command + optional env
fs = { command = ["mcp-server-filesystem", "/workspace"] }
build = { command = ["cargo-mcp"], env = { CARGO_HOME = "/workspace/.cargo" } }

shell-mcp-command is a placeholder. Replace it with the shell MCP server you install in the image, or declare any other MCP command that should run inside the container.

  • command (array of strings, required unless using short form): argv of the MCP server.
  • env (table str->str, optional, default: {}): env vars set on the podman exec invocation.

Notes:

  • The first element of command must be on $PATH inside the container, or absolute.
  • The map key (e.g. fs, shell, build) is the server name. It must match ^[a-zA-Z][a-zA-Z0-9_-]*$ and be unique within an image-config.
  • The server name appears in outrig logs <session> <server> and as the prefix on every tool the server advertises (<server>__<tool>).
  • Each env value is either a literal string forwarded verbatim or a ${VAR} reference resolved from the host environment at MCP startup – see the subsection below.
  • Images can provide the same table via their org.outrig.mcp OCI label. Repo config entries override image entries by server name; see Concepts -> MCP Servers.
  • Build-from-Dockerfile repo images are stamped with the merged org.outrig.mcp label on cache misses, so outrig image inspect <name>:<content-hash> can show their declared repo-local MCP entries without starting a container.

MCP env value syntax

Each entry on the right-hand side of an env table is one of:

build = { command = ["cargo-mcp"], env = {
  CARGO_HOME = "/workspace/.cargo",  # literal -- forwarded to podman as-is
  GH_TOKEN   = "${GITHUB_TOKEN}",    # reference -- resolved from host env at MCP startup
  STILL_LIT  = "${lower_case}",      # literal -- doesn't match ^[A-Z_][A-Z0-9_]*$
  ALSO_LIT   = "prefix-${X}-suffix", # literal -- embedded substitution is not supported
} }

The reference form is exactly "${VAR}", where VAR matches ^[A-Z_][A-Z0-9_]*$ – the same syntax api-key accepts. Anything else is treated as a literal and passed through verbatim, including malformed-looking references (lower-case names, unmatched braces, or embedded substitution). If the named host env var is unset when the MCP server is about to start, MCP startup fails with an error naming the variable, the server, and the env key.

See Concepts -> MCP Servers.

Resolution: which file wins

Both files are loaded; entries are merged by name. Repo entries win over global entries. A name defined only once is straightforward; a name defined in both means the repo’s definition is used in full (no per-key merging).

~/.outrig/config.toml             .agents/outrig/config.toml         effective
[providers.openai]                                                   global value
[providers.local]                 [providers.local]                  repo overrides
                                  [providers.staging]                repo only

outrig run walks the chain at startup: agent -> model (explicit or default-model) -> provider -> resolved api-key from env. Anything that fails to resolve is an error printed to stderr before the REPL starts.

[workspace] primary fields are repo-owned: the repo config’s host-path and container-path win as a block. Extra workspace.mounts are combined instead of replaced: global mounts are kept first, followed by repo mounts. Duplicate final container-path values are rejected during validation.

[network].mode follows repo precedence when the repo config declares the table. If the repo omits [network], the global mode remains in effect. This matters when global config enables audit or filter mode and a repo explicitly sets mode = "default". Network policy keys are global-only; repo config cannot set network.default, network.allow, or network.deny.

Full examples

Global ~/.outrig/config.toml

default-model    = "fast"
session-root     = "/var/lib/outrig/sessions"   # optional; default = XDG data dir
model-cache-root = "/var/cache/outrig/models"   # optional; default = XDG cache dir
tool-call-max    = 100                           # optional; default = 50
tool-result-max  = 262144                        # optional; default = 256 KiB

[network]
mode = "default"                                 # optional; default, audit, or filter
default = "deny"                                 # optional for filter mode
allow = ["github.com:443", "*.npmjs.org"]        # optional; global only
deny  = ["*:22"]                                 # optional; global only

[providers.openai]
style    = "openai"
base-url = "https://api.openai.com/v1"
api-key  = "${OPENAI_API_KEY}"

[providers.local]
# requires `cargo build --features local-llm` to actually use, but always parses.
style = "mistralrs"

[models.fast]
provider   = "openai"
identifier = "gpt-4o-mini"

[models.smart]
provider   = "openai"
identifier = "gpt-4o"

[models.phi3-fast]
provider   = "local"
model-id   = "microsoft/Phi-3-mini-4k-instruct-gguf"
model-file = "Phi-3-mini-4k-instruct-q4.gguf"
device     = "cpu"

Repo .agents/outrig/config.toml

default-image = "coding"
default-agent     = "coding"

[workspace]
host-path      = "."
container-path = "/workspace"

[[workspace.mounts]]
host-path      = "../shared-docs"
container-path = "/resources/shared-docs"

[[workspace.mounts]]
host-path      = "/var/tmp/outrig-cache"
container-path = "/resources/cache"
access         = "read-write"

[agents.coding]
# model omitted -> uses global default-model = "fast"
image       = "coding"
preamble    = "You are a careful coding assistant. Repo is at /workspace."
temperature = 0.2
tool-call-max = 300
tool-result-max = 1048576

[agents.review]
model    = "smart"        # explicit override
preamble = "You are a meticulous code reviewer."

[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"
context    = ".agents/outrig/images/coding"
build-args = { NODE_VERSION = "20" }

  [images.coding.security]
  capability-profile = "no-net-raw"
  cap-drop = ["MKNOD", "SETFCAP"]
  cap-add  = ["NET_BIND_SERVICE"]

  [images.coding.mcp]
  fs    = { command = ["mcp-server-filesystem", "/workspace"] }
  shell = ["bash", "-lc", "exec shell-mcp-command"]
  build = { command = ["cargo-mcp"], env = { CARGO_HOME = "/workspace/.cargo" } }

Validation rules

outrig run and outrig mcp use the full validation path. outrig build validates every image-config in the merged config but does not require agent/model/provider wiring to resolve.

  • default-image must name an existing [images.<name>] block.
  • default-agent must name an existing [agents.<name>] block.
  • Every agents.<name>.model (if set) must name an existing [models.<name>]. If model is omitted, default-model must be set and must name an existing [models.<name>].
  • Every models.<name>.provider must name an existing [providers.<name>].
  • Every agents.<name>.image (if set) must name an existing [images.<name>].
  • Every providers.<name>.style must be one of {"openai", "mistralrs"}. Other styles are reserved for future Rig adapters and listed as TODO in the providers concept page. The build-time feature gate (--features local-llm) is not checked at validate time – see “Always parses, even without --features local-llm” above.
  • Every providers.<name>.api-key (on style = "openai") must match ^\$\{[A-Z_][A-Z0-9_]*\}$.
  • Every [models.<name>] whose provider has style = "openai" must set identifier and must not set any of model-id, model-path, model-file, revision, context-length, device.
  • Every [models.<name>] whose provider has style = "mistralrs" must set exactly one of model-id / model-path. When model-id is set, model-file is required – mistralrs’s GGUF loader needs a specific filename and HF repos typically hold many quantizations. model-file accepts either a single string (one GGUF file) or an array of strings (a multi-shard quantization, e.g. *-00001-of-00003.gguf). revision is optional and only meaningful with model-id. A model-path, if set, must exist on disk relative to the repo root (or be absolute). identifier is not allowed on mistralrs models. device, if set, must be one of cpu, cuda, cuda:N, or metal.
  • model-cache-root, if set, must be an absolute path; outrig creates it if missing.
  • tool-call-max, if set at the top level or on an agent, must be between 1 and 2000.
  • tool-result-max, if set at the top level or on an agent, must be between 1024 and 16777216 bytes.
  • [network].mode, if set, must be default or audit.
  • Every server name in [images.<name>.mcp] must match ^[a-zA-Z][a-zA-Z0-9_-]*$ and be unique within its image-config.
  • Every command array must be non-empty.
  • dockerfile and context must exist on disk relative to the repo root (build path only).
  • Each [images.<name>] must set exactly one of: image-name, or dockerfile + context. Setting both shapes, neither, image-name with build-args, or only one of dockerfile/context without the other is an error.
  • image-name must not be empty.
  • A build-from-Dockerfile [images.<name>] block key must be a valid container image repository component – lowercase alphanumeric separated by ., _, or - (^[a-z0-9]+([._-]+[a-z0-9]+)*$) – because it becomes the built image’s repository. Image-name configs are exempt: their block key is just a label.
  • Every [images.<name>.security].capability-profile, if set, must be one of default, no-net-raw, or drop-all.
  • Every capability name in cap-drop or cap-add must be non-empty and match ^[A-Z0-9_]+$ after optional CAP_ stripping.
  • Capability names must not be duplicated within cap-drop or within cap-add, after optional CAP_ stripping.
  • The same normalized capability name must not appear in both cap-drop and cap-add.
  • session-root, if set, must be an absolute path; outrig creates it if missing.
  • Every workspace.mounts[*].host-path, if validated with a repo root, must exist and be a directory. Relative host paths resolve against the repo root.
  • Every workspace.mounts[*].container-path must be absolute and must not be /.
  • Extra workspace mount container-path values must be unique, including no collision with the primary workspace container-path.
  • Every workspace.mounts[*].access, if set, must be either read-only or read-write.
  • Unknown keys at any level are rejected.

See also

  • Concepts – narrative explanations of what these keys mean.
  • Reference -> CLI – flags that override or interact with config.