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 container 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

TODO: Incompletecargo install outrig-cli is not yet on crates.io.

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 container add for the first container-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 container 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 [containers.<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 container
? Container 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/containers/hello-outrig-standard/Dockerfile
[outrig] added [containers.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
        └── containers/
            └── 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-container = "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."

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

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

.agents/outrig/containers/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 – container 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] container-config: hello-outrig-standard
[outrig] dockerfile:       .agents/outrig/containers/hello-outrig-standard/Dockerfile
[outrig] context:          .agents/outrig/containers/hello-outrig-standard
[outrig] cache key:        outrig-cache:8c2a4f7e91d6b5a3
[buildah] STEP 1/N: FROM docker.io/library/debian:bookworm-slim
...
[buildah] Successfully tagged outrig-cache:8c2a4f7e91d6b5a3
[outrig] image ready

A second outrig build is a cache hit:

$ outrig build
[outrig] image ready (cache hit: outrig-cache: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 cap:     50
[outrig] tool-result cap:   262144 bytes
[outrig] container-config:  hello-outrig-standard
[outrig] image:             outrig-cache: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 [containers.<name>] config block, named container-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 + container). 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 container add writes container files under .agents/outrig/containers/<name>/:

.agents/outrig/
├── config.toml
└── containers/
    └── 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 [containers.<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 [containers.<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 [containers.<name>] config block

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

default-container = "coding"

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

  [containers.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 [containers.<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:

[containers.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:

[containers.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:

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

  [containers.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 --container scratch pulls the image if not already local:

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

On subsequent runs when the image is already present:

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

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

Named container-configs

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

default-container = "coding"

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

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

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

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

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

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

Image caching

outrig tags built images as outrig-cache:<hash>. The hash combines the contents of the Dockerfile, the build-args, and the content of the build context (gitignore-aware when the context is in a git repo, otherwise a tarball hash).

A change to the Dockerfile or any file in the context 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 outrig-cache:<hash> tag in that path. --no-cache on an image-name config re-runs podman pull even when the image is already present locally.

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 container 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 [containers.<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-container, as a map keyed by the server’s local name:

[containers.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 container 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 a container-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 ship its MCP declarations at /etc/outrig/container.toml:

# /etc/outrig/container.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 [containers.<name>.mcp]. At session startup, outrig reads the image file after the container starts, 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.

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 [containers.<name>.mcp].

An /etc/outrig/container.toml is not required; this allows a shared container to be used with different configurations via config.toml. Malformed TOML, 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 --container coding

The command starts the selected container, reads the embedded file, 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 container-config’s purpose. A planning container probably doesn’t need shell. A coding container 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 container-configs.
  • Sessionsoutrig logs <session> <server> for stderr.
  • Reference -> Config – full schema for the [containers.<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 container-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 container).

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 container 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["[containers.coding]"]
    end
    user --> prov
    m1 --> prov
    m2 --> prov
    a1 --> m1
    a2 --> m2
    a1 -. "container" .-> 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 container.

[agents.coding]
# model omitted -> falls back to top-level default-model
container   = "coding"
preamble    = "You are a careful coding assistant. Repo is at /workspace."
temperature = 0.2
tool-call-cap = 300
tool-result-cap = 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). container is optional too: if set, outrig run --agent <name> defaults to that container-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-cap also lives on the agent when a role needs longer tool loops. If unset, the agent uses the top-level tool-call-cap, then the compiled-in default of 50. The cap is per user turn, so typing a follow-up prompt starts a fresh count while keeping conversation history. tool-result-cap works the same way for oversized MCP output: an agent can inherit the top-level cap or set its own byte cap 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
[containers.<name>]repo only
default-modeltypical homeoptional override
default-agentrarerequired for outrig run
default-containerrarerequired 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 container add to scaffold containers.
  • outrig config – group of commands for outrig’s configuration. v0: config init (writes the global ~/.outrig/config.toml).
  • outrig container – group of commands for container-configs. v0: container add (scaffolds a Dockerfile under .agents/outrig/containers/<name>/).
  • AI-assisted design – use outrig mcp self when the built-in container templates do not fit.
  • outrig run – start an interactive agent session. The main subcommand.
  • outrig mcp – expose a container-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 container-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 container 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 container loop is always offered (you may want to add another container-config later).

Synopsis

outrig init [--force]
FlagDefaultDescription
--forceoffOverwrite existing files. Propagates to config init and container 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 container
? Container 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 container-config name is <repo-folder>-standard (kebab-cased), so the container 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 container loop:

? Add a container-config now? [Y/n]:

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

? Add another container-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 (containers will be filled in by container add):

default-container = "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-container Dockerfile and [containers.<name>] block come from outrig container 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.
  • Container loop -> always offered, since adding more containers 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 container 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 container

outrig container groups commands that manage container-configs (the named Dockerfile + MCP-server bundles that host the agent’s tools). In v0 only outrig container add is implemented; the rest of the group (container ls, container rm) is reserved for later.

outrig container add

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

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

Bootstrapping a fresh repo

If you run outrig container 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 container 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 container add [<name>] [--force]
Argument / flagDefaultDescription
<name>promptedContainer-config name (becomes [containers.<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 container-config name is <repo-folder>-standard (kebab-cased), so the example below assumes a hello-outrig repo.

$ cd hello-outrig
$ outrig container add
? Container-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/containers/hello-outrig-standard/Dockerfile
[outrig] added [containers.hello-outrig-standard] block to .agents/outrig/config.toml
[outrig] added [containers.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/container.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 [containers.<name>.mcp] block directly – see Concepts -> MCP Servers. A shell MCP server is the usual next tool for coding containers; choose a package, install it in the Dockerfile, and declare its command in [containers.<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 a container 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 [containers.<name>] block without being limited to the built-in template menu.

What gets written

.agents/outrig/containers/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:

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

  [containers.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 container add hello-outrig-standard
error: .agents/outrig/containers/hello-outrig-standard/Dockerfile
       already exists; pass --force to overwrite.

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

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 a container-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 for container config and MCP server entries.
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 container blocks.

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 container-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 [containers.<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.

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 container-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

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 container-config, and drops you into a stdin/stdout REPL with the agent.

Synopsis

outrig run [--agent <name>]
           [--container <name>]
           [--config <path>]
           [--device <cpu|cuda|cuda:N|metal>]
           [--max-tool-calls <n>]
           [--max-tool-result-bytes <n>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--session-root <path>]
           [--verbose]
  • --agent <name> (default: default-agent): selects an [agents.<name>] block.
  • --container <name> (default: agent’s container, else default-container): pick a container.
  • --config <path> (default: walks up from cwd): use from outside the repo or non-standard locations.
  • --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-cap, else 50): override the per-turn tool-call cap for this run.
  • --max-tool-result-bytes <n> (default: resolved tool-result-cap, else 262144): override the per-tool-result byte cap 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.
  • --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.

What happens, in order

  1. Locate config. Walks up from the current directory until .agents/outrig/config.toml is found, or fails.
  2. Resolve container-config. Uses --container if given, otherwise default-container. The selected block must exist.
  3. Build (or cache-hit) the image. Runs buildah build. If the cache hash matches an existing tag, no rebuild.
  4. Start the container. podman run -d --rm --name outrig-<sid> -v <repo>:/workspace:rw --userns=keep-id ... <image> sleep infinity.
  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 [containers.<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), look up [agents.<a>].model – if unset, fall back to top-level default-model. Then [models.<m>].provider, then [providers.<p>]. Resolve the per-turn tool-call cap from tool-call-cap and the per-result byte cap from tool-result-cap. 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 container
[outrig] agent/container resolved: agent coding, container coding (0ms)
[outrig] computing image tag
[outrig] image tag computed: outrig-cache:8c2a4f7e91d6b5a3 (32ms)
[outrig] ensuring image outrig-cache:8c2a4f7e91d6b5a3
[outrig] image ready: outrig-cache: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 cap:     50
[outrig] tool-result cap:   262144 bytes
[outrig] container-config:  coding
[outrig] image:             outrig-cache: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 cap. The compiled-in default is 50; a top-level tool-call-cap in config changes the default, [agents.<name>].tool-call-cap overrides it for one agent, and outrig run --max-tool-calls <n> overrides both for the current run. The cap is per turn, so a follow-up prompt starts a fresh count.

Each individual tool result also has a byte cap before it is appended to the LLM-visible conversation history. The compiled-in default is 262144 bytes (256 KiB); a top-level tool-result-cap in config changes the default, [agents.<name>].tool-result-cap 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 cap, outrig keeps the head of the result and appends a marker that includes the original size, the cap, and a hint to narrow the next query.

When the tool-call cap 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 cap was reached:

[outrig] tool-call iteration cap (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 cap. 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] container-config: coding
error: container-config "coding" is missing required key: dockerfile

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

$ outrig run
[outrig] container-config: coding
[outrig] image:            outrig-cache: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
[outrig] container-config: coding
[outrig] image:            outrig-cache: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 container-config into one MCP server for an external client. It starts or attaches to the selected container, launches every [containers.<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 [--container <name>]
           [--attach <session-id-or-container-name>]
           [--listen <addr>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--config <path>]
           [--global-config <path>]
           [--session-root <path>]
           [--verbose]

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

outrig mcp self
  • --container <name> (default: default-container): selects a [containers.<name>] block. 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): 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.
  • --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 a container-config. See AI-assisted design.

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

  1. --container <name>
  2. top-level default-container

If neither is set, startup fails with:

error: no --container or default-container configured

With --attach, container-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 container_config_name.
  2. If --container <name> is also passed, it overrides the session row’s container_config_name.
  3. If the attach value is not a known session id, outrig treats it as a podman container name and requires --container <name>.

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

The selected container must expose at least one backing MCP server after image /etc/outrig/container.toml entries and [containers.<name>.mcp] overrides are merged. A container-config 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 a container-only repo config:

default-container = "coding"

[workspace]
root = "."

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

[containers.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 at /etc/outrig/container.toml. 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 --container coding

In fresh mode this starts the selected container, reads /etc/outrig/container.toml, applies config.toml overrides, prints the merged [mcp] table to stdout, then stops the container. 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 --container 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 \
  --container 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",
        "--container",
        "coding"
      ],
      "env": {}
    }
  }
}

Zed uses context_servers in its settings:

{
  "context_servers": {
    "outrig": {
      "command": "outrig",
      "args": [
        "mcp",
        "--config",
        "/path/to/repo/.agents/outrig/config.toml",
        "--container",
        "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 --container 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 --container 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 container-config. Uses --container if given, otherwise top-level default-container. There is no agent.container step – this subcommand has no agent.
  3. Prepare the container. Fresh mode builds or cache-hits the image and 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 /etc/outrig/container.toml from the image if present, then overlay [containers.<name>.mcp] from config by server name.
  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] container-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  CONTAINER  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 container-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 [--container <name>]
             [--config <path>]
             [--no-cache]
             [--all]
  • --container <name> (default: default-container): build a specific named container-config.
  • --all (default: off): build every container-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.
  2. For each selected container-config:
    • Computes the cache key (blake3 over Dockerfile content + resolved build-args + 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:
      buildah build --tag outrig-cache:<hash> \
                    --file <dockerfile> \
                    [user build-args] \
                    <context>
      

Examples

Build the default:

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

Cache hit on the second run:

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

Build a specific container-config:

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

Build every container-config in one go:

$ outrig build --all
[outrig] container-config: coding   -> outrig-cache:8c2a4f7e91d6b5a3 (cache hit)
[outrig] container-config: planning -> outrig-cache: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 container-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  CONTAINER  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 / CONTAINER / 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

TODO: Incomplete – every command and behavior on this page describes outrig’s intended behavior; the implementation isn’t ready yet.

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 container-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 container) and pick with --agent:

default-container = "coding"
default-agent     = "coding"

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

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

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

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

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

  [containers.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 container-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 container-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 container add in a loop.

outrig init [--force]
FlagDefaultDescription
--forceoffOverwrite existing files. Propagates to config init and container 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 container add

Interactively scaffold a container-config: writes a Dockerfile under .agents/outrig/containers/<name>/Dockerfile and adds the matching [containers.<name>] and [containers.<name>.mcp] blocks to the repo config. The first subcommand of the outrig container group; future subcommands (container ls, container rm) are deferred.

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

See Usage -> outrig container.

outrig run

Start an interactive agent session.

outrig run [--agent <name>]
           [--container <name>]
           [--config <path>]
           [--device <cpu|cuda|cuda:N|metal>]
           [--env <KEY=VALUE>]
           [--global-config <path>]
           [--max-tool-calls <n>]
           [--max-tool-result-bytes <n>]
           [--network <default|audit|filter>]
           [--session-dir <path>]
           [--session-root <path>]
           [--verbose]
  • --agent <name> (default: default-agent): selects an [agents.<name>] block.
  • --container <name> (default: from agent or default-container): container-config to launch.
  • --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-cap, else 50): per-turn tool-call cap.
  • --max-tool-result-bytes <n> (default: resolved tool-result-cap, else 262144): per-tool-result byte cap.
  • --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.
  • -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.

See Usage -> outrig run for REPL details.

outrig mcp

Serve the selected container-config’s backing MCP servers as one MCP server over stdio.

outrig mcp [--container <name>]
           [--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>]
           [--verbose]

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

outrig mcp self
FlagDefaultDescription
--container <name>default-containerContainer-config to launch.
--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.
-v, --verboseoffPrint container lifecycle traces.

There is no --agent flag. outrig mcp does not resolve default-agent, does not let agent.container participate in container selection, and does not read provider API keys. Container selection is --container, then top-level default-container, then an error.

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 container-config. If there is no session match, the value is treated as a podman container name and --container <name> 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 image /etc/outrig/container.toml plus [containers.<name>.mcp] overrides. 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 container selection and setup path, but exits after printing the effective [mcp] table to stdout. It is for debugging embedded image config 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 container 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 container-config images, without starting an agent.

outrig build [--container <name>]
             [--all]
             [--no-cache]
             [--config <path>]
  • --container <name> (default: default-container): build a specific named container-config.
  • --all (default: off): build every container-config. Mutually exclusive with --container.
  • --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 [containers.<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], [containers.<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-container = "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-cap     = 100                               # optional; defaults to 50
tool-result-cap   = 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-containerstringfor outrig runrepoDefault --container.
default-agentstringfor outrig runrepoDefault --agent.
default-modelstringif agent omits modelglobalFallback model name.
session-rootpathnoglobalSessions root dir.
model-cache-rootpathnoglobalGGUF download cache dir.
tool-call-capintegernoglobalPer-turn tool-call cap.
tool-result-capintegernoglobalPer-tool-result byte cap.
network.modestringnoeitherNetwork mode.
network.defaultstringnoglobalFilter fallback action.
network.allowarraynoglobalFilter allow entries.
network.denyarraynoglobalFilter deny entries.

default-container and default-agent belong in the repo config – containers and agents are project-scoped. default-model, session-root, model-cache-root, and tool-call-cap belong in the global config since they’re user/machine-level. tool-result-cap 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-cap 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-cap overrides the top-level value for one agent, and outrig run --max-tool-calls <n> overrides both for one invocation.

tool-result-cap 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-cap 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 cap are truncated at a UTF-8 boundary and end with an [outrig: tool result truncated] marker that reports the original size and cap.

[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-secsintegerno120HTTP timeout for LLM calls.

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 a container so outrig run --agent <name> knows which sandbox to use.

[agents.coding]
# model omitted -> falls back to top-level default-model
container = "coding"
preamble  = "You are a careful coding assistant. Repo is at /workspace."
temperature = 0.2
max-tokens  = 4096
tool-call-cap = 300
tool-result-cap = 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.
  • container (string, optional, default: default-container): default container-config to launch.
  • temperature (float, optional, default: provider default): sampling temperature.
  • max-tokens (integer, optional, default: provider default): output token cap per turn.
  • tool-call-cap (integer, optional, default: top-level value or 50): tool calls per turn.
  • tool-result-cap (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. When outrig run --agent <a> runs, the chosen container is --container if given, otherwise agents.<a>.container if set, otherwise default-container. tool-call-cap is per turn, not per session; follow-up prompts start a fresh count. tool-result-cap 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.

[containers.<name>]

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

Build-from-Dockerfile (existing form)

[containers.coding]
dockerfile = ".agents/outrig/containers/coding/Dockerfile"
context    = ".agents/outrig/containers/coding"
build-args = { NODE_VERSION = "20" }
  • 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)

[containers.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 container add writes its output under .agents/outrig/containers/<name>/. You can put Dockerfiles anywhere you want by editing these paths; the .agents/outrig/containers/ 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.

[containers.<name>.security]

Optional runtime security controls for the selected container:

[containers.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.

[containers.<name>.mcp]

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

[containers.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 a container-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 at /etc/outrig/container.toml. Repo config entries override image entries by server name; see Concepts -> MCP Servers.

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-cap    = 100                           # optional; default = 50
tool-result-cap  = 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-container = "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"
container   = "coding"
preamble    = "You are a careful coding assistant. Repo is at /workspace."
temperature = 0.2
tool-call-cap = 300
tool-result-cap = 1048576

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

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

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

  [containers.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

  • default-container must name an existing [containers.<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>.container (if set) must name an existing [containers.<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-cap, if set at the top level or on an agent, must be between 1 and 2000.
  • tool-result-cap, 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 [containers.<name>.mcp] must match ^[a-zA-Z][a-zA-Z0-9_-]*$ and be unique within its container-config.
  • Every command array must be non-empty.
  • dockerfile and context must exist on disk relative to the repo root (build path only).
  • Each [containers.<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.
  • Every [containers.<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.