Embedded-Focus/agent-circus

Run AI coding agents in sandboxed containers communicate via ACP

32

stars

225

commits

Python

primary language

Sep 14, 2026

updated

embedded-focus.com/blog/agent-circus/
ai-agents
containerization
security

README

AI Agents Circus

AI Agents Circus

Run AI coding agents in isolated Docker or Podman containers with one config language.

License: MIT CI Security Dependabot Python 3.11+ Docker Podman uv Last commit Agents: Claude Code · Codex · Vibe CLI · OpenCode

Open Protocols · Getting Started · Working with the Environment · Authentication · Configuration · Container Runtimes · Hooks · Editors · Roadmap

Run AI coding agents in sandboxed containers with full control over what they can see, reach, and inherit from your host environment.

Docker and Podman are supported container runtimes. Docker is the default; Podman, including rootless operation, can be selected from the CLI, environment, or project configuration.

Agent Circus wraps each agent in its own container, giving you a reproducible, isolated environment that works across machines and projects. You decide which files agents can access, secrets stay on the host, and a built-in firewall restricts outbound network access to known-good destinations.

It also provides a universal configuration mechanism for operating agent harnesses consistently. Authentication, Git identity, SSH agent forwarding, MCP servers, environment variables, extra mounts, hooks, ports, and data stores are declared once in project config and applied across supported agents.

Getting started takes two commands. No files are written to your project, no manual container setup required. Customize per-project only when you need to.

Currently supported agents:

IDEs interface with agents via the Agent Client Protocol (ACP).

Open Protocols

Agent Circus is built around open interfaces instead of editor- or vendor-specific integrations:

Open protocols map showing ACP and MCP relationships

Getting Started

Installing the agent-circus Tool

The host CLI requires Python 3.11 or newer and is tested on Python 3.11–3.14. The agent containers manage their own runtimes; the Mistral Vibe image uses Python 3.14 independently of the Python running the host CLI.

Install the published package from PyPI with uv:

uv tool install agent-circus

It is also possible to use agent-circus without installing it explicitly. For example:

uvx agent-circus version

Alternatively, install it into a Python 3.11 or newer virtual environment using pip:

python3.11 -m venv agent-circus-venv
. agent-circus-venv/bin/activate
python -m pip install agent-circus

The agent-circus command is available while this virtual environment is activated. Activate it again in a new shell before using the tool. On Linux, the venv module may be provided as a separate package by your distribution.

You can use agent-circus from any project directory; cloning this repository is not required. Docker or Podman must be installed on the host to build and run the agent containers.

To install from a source checkout for development instead, run this command in the repository root:

uv tool install .

Version Information

The version command includes the full Git commit. For an installation from a local checkout, it reports that checkout's current HEAD. Distribution artifacts, including packages installed from PyPI, contain the commit recorded when they were built.

Shell Completions

agent-circus completion <shell> prints a completion script for bash, zsh, fish, powershell, or pwsh (defaults to bash), following the <cmd> completion <shell> convention used by tools like kubectl and gh. Source it directly in your shell startup file:

source <(agent-circus completion bash)

Alternatively, for bash, agent-circus completion bash --install writes the script to ~/.bash_completion.d/agent-circus, a directory many bash-completion setups auto-source, creating it if missing and leaving it untouched if already up to date:

agent-circus completion bash --install

If you use a shorter alias for agent-circus (e.g. alias ac=agent-circus), pass --alias (bash only) to also define the alias and wire up its completion in the same script:

agent-circus completion bash --install --alias ac

See the uv tool documentation on how to work with tools in general.

After installing you can start right away in one of your projects (instant mode):

# Docker (default)
agent-circus build
agent-circus exec claude-code -- claude-agent-acp

# Podman
agent-circus build --runtime podman
agent-circus exec --runtime podman claude-code -- claude-agent-acp

The exec command automatically starts the container if it is not running yet. There is no separate up step needed.

Agent Circus is not tied to ACP or editor integrations: the command after -- can also be a direct agent CLI such as codex, or a shell such as bash, when you want to work inside the harness container.

Note: Auto-started containers are not automatically removed when they are no longer in use. Run agent-circus remove to clean up idle containers.

Upgrading and Uninstalling

For an installation managed by uv, upgrade or uninstall the tool with:

uv tool upgrade agent-circus
uv tool uninstall agent-circus

For an installation in an activated virtual environment, use:

python -m pip install --upgrade agent-circus
python -m pip uninstall agent-circus

Working with the Environment

There are two modes of operation:

Instant Mode (Preferred)

Instant mode uses the templates bundled in the agent-circus package directly. No files are written to your project directory. Just run commands against any workspace:

# Build container images
agent-circus build

# Execute a command in a container (starts it automatically)
agent-circus exec claude-code -- claude-agent-acp
agent-circus exec codex -- codex-acp
agent-circus exec opencode -- opencode
agent-circus exec -T claude-code -- echo hello   # non-interactive

# Optionally start containers ahead of time
agent-circus up                           # start all services
agent-circus up claude-code               # start a single service

# Show status of agent containers
agent-circus ps

# Remove all related resources
agent-circus remove
agent-circus remove --volumes             # also remove named volumes
agent-circus remove --force               # don't ask for permission

Updating container dependencies

Each Agent Circus release includes a baseline set of agent and tool versions. You can select newer upstream versions without upgrading the CLI:

agent-circus deps show
agent-circus deps update --dry-run
agent-circus deps update
agent-circus build
agent-circus up

deps update updates the complete supported dependency set: npm, uv, yq, Claude Code, Claude Agent ACP, Bun, Claude-Mem, Codex, Codex ACP, OpenCode, and Mistral Vibe, including its Python transitive dependencies. It accepts stable versions and does not automatically downgrade existing dependencies. The Node base image and apt packages are outside this updater's scope.

Host requirement: uv

Applying an update requires the uv executable on the host. Install it before using deps update; no particular host Python version needs to be installed:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

The operation also needs network access to GitHub, the Python package registry, and the Python download service. It runs entirely on the host and does not start or execute code inside an agent container. Agent Circus runs the following command in a disposable copy of the effective container recipe:

uv lock --upgrade --prerelease if-necessary --python 3.14 --no-config

Mistral Vibe supports Python 3.12 and newer. Agent Circus deliberately runs it on Python 3.14 to use the current Python runtime in the controlled container environment. For lock resolution, uv uses an existing Python 3.14 interpreter or automatically downloads a managed interpreter to its host cache. This does not require a system-wide Python installation or manual preparation. Automatic setup can fail if downloads are disabled, for example with UV_PYTHON_DOWNLOADS=never, or if the download service cannot be reached. To diagnose that configuration directly, run:

uv python find 3.14

The command only generates a lockfile; it does not create or synchronize a host project environment. The candidate recipe is temporary, and the active dependency selection changes only after every lookup, lock, and validation step succeeds. Direct tool versions must be stable. Python transitive dependencies may use a prerelease when it is required by a selected stable package; Mistral Vibe currently has such a dependency. --dry-run only checks direct tool versions; it does not prepare Python 3.14 or resolve Python transitive dependency changes.

If uv fails, Agent Circus prints its captured output after removing likely credentials. To retain diagnostics from a retry, put the global logging options before the subcommand:

agent-circus --log-file /tmp/agent-circus.log deps update

There is no persistent per-operation log unless --log-file, LOGFILE, or the user-global logging configuration is set. Output from an earlier invocation that did not use one of those options cannot be recovered.

Updates take effect only after rebuilding images and recreating containers. Run agent-circus up after build to let Compose recreate containers whose images changed. Existing running containers are not changed by deps update. You can also use agent-circus up --build for the build/start steps.

Authentication

GitHub discovery uses GITHUB_TOKEN, falling back to GH_TOKEN, or anonymous access if neither is set. For example, with a token already set in your shell:

agent-circus deps update --dry-run

The token needs access to public release metadata; no repository write access is needed. Authentication and API rate-limit errors leave the active dependency selection unchanged. Tokens are not saved in snapshots or added to container build arguments. npm and PyPI discovery use public registries; private registry configuration and automatic reuse of gh auth credentials are not supported by this feature.

Selection scope and history

In instant mode, selections live under $XDG_DATA_HOME/agent-circus/dependencies/, defaulting to ~/.local/share/agent-circus/dependencies/. An update affects subsequent builds in all instant-mode workspaces for that user.

In deploy mode, selections live in .agent-circus/dependencies/ in the project and take precedence over user-wide selections. A deployed project with no selection uses the versions in its deployed templates. You can commit its selection.json and snapshots/ directory to share exact selections with your team; exclude the .lock file. All commands accept --workspace PATH (-w PATH) after the subcommand.

agent-circus deps history
agent-circus deps rollback
agent-circus deps reset
agent-circus deps show --workspace /path/to/project

rollback selects the previous snapshot; repeating it switches back to the selection you just left. reset selects the installed CLI release's baseline. Both operations work without upstream access or host uv. Rebuild and run up after either operation to change the containers.

Snapshots contain a version manifest (versions.toml), pyproject.toml, and uv.lock. They are immutable and identified by their content hash. Successful updates save the previous selection, including the bundled baseline, before atomically activating the new snapshot. Failed updates leave the active selection intact; unchanged updates do not duplicate snapshots or discard the rollback target. Snapshots are retained until you manually remove unused ones. Do not edit snapshot contents or remove active/previous snapshots.

deps show distinguishes the release baseline from locally resolved upstream versions. Successful upstream resolution does not establish that the new agents work together. Snapshots preserve versions, not downloaded artifacts or container images: rebuilding still requires upstream artifacts to be available. They also do not lock every npm transitive dependency, apt package, or base-image digest, so they do not guarantee byte-for-byte reproducible images.

CLI upgrades and deployed templates

Instant mode uses a new release's baseline after a CLI upgrade when no explicit selection is active. Explicit selections remain active if their recipe format is compatible; incompatible selections produce an error instead of being silently replaced. Use deps reset to switch to the installed release's baseline. Saved baseline snapshots remain available for rollback when compatible. Deployed projects keep their files and selection across CLI upgrades; use deps reset explicitly to select the new baseline.

Updates never overwrite deployed Dockerfiles or Compose files. Builds use a temporary copy and a build override that preserves the original Compose path base. This first implementation recognizes recipes matching the installed release except for supported version pins. Custom hook scripts and other copied files are preserved; changes to Dockerfile, Compose structure, or Python project configuration require migration before using a dependency selection.

To migrate an unsupported deployment, back up your customizations, redeploy with agent-circus init --deploy --force, and reapply supported customizations through hooks and config.toml. The force option overwrites deployed template files, including hooks, so keep your backup until migration is complete.

Maintainers can continue using make update-templates-dryrun and make update-templates to prepare the next release's bundled baseline. These source-maintenance targets are separate from end-user dependency selections. The manual Dependency container smoke tests GitHub Actions workflow builds both template modes and checks agent version commands against the baseline or newly resolved upstream dependencies; run it before accepting a release baseline.

Deploy Mode

Deploy mode copies configuration files into a .agent-circus/ directory inside your project. Use this if you need to customize the Dockerfile or compose.yaml per project.

# Deploy configuration files to the workspace
agent-circus init --deploy

# All other commands work the same; deploy mode is auto-detected
agent-circus build
agent-circus exec claude-code -- claude-agent-acp

# Remove containers and deployed files
agent-circus remove --destroy             # remove containers + .agent-circus/
agent-circus remove --destroy --force     # don't ask for permission

When both a deployed .agent-circus/ directory and instant mode are available, deploy mode takes priority.

Authentication

Each agent authenticates against its vendor's API using the credentials already present on the host. Agent Circus seeds a private, project-local writable copy of each vendor configuration directory and mounts that copy into the corresponding container:

AgentSeed sourceContainer path
Claude Code~/.claude/home/node/.claude
Codex~/.codex/home/node/.codex
Vibe CLI~/.vibe/home/node/.vibe
OpenCode~/.config/opencode/home/node/.config/opencode

The copies are stored under ~/.local/state/agent-circus/<project>/agent-config/<agent>/ with directory permissions restricted to the current user. They are seeded only once, so container changes do not modify host configuration and later host changes are not copied automatically. Agent Circus merges generated settings such as MCP servers into these writable copies before startup.

To discard a copy and seed it again from the current host configuration on the next start, first stop the affected service and run:

agent-circus config reset codex
agent-circus config reset --all

Use --force to skip the confirmation prompt. Reset refuses to delete a config store while its service is running.

For credential refresh or other explicit edits to the real host provider configuration, run a command with --host-config. This bypasses the project-local copy and mounts the host configuration directory writable for that single service. Agent Circus does not merge generated MCP settings into the host configuration while this mode is active.

agent-circus remove --force codex
agent-circus exec codex --host-config -- codex login
agent-circus remove --force codex
agent-circus config reset codex --force
agent-circus up codex

The second remove is needed because exec starts a stopped service and leaves it running. After config reset, the next normal startup seeds a fresh project-local copy from the updated host configuration.

Configuration

Agent Circus can be configured via TOML files. Settings are resolved in this order (last wins):

  1. User-global$XDG_CONFIG_HOME/agent-circus/config.toml (default: ~/.config/agent-circus/config.toml)
  2. Project-local.agent-circus/config.toml in the workspace

CLI flags override both.

[!IMPORTANT] A key = value line belongs to the last [table] header above it, blank lines or not. A top-level key placed after [runtime], [hooks], [ssh], etc. silently becomes part of that table instead of standing on its own, with no error or warning:

[runtime]
engine = "podman"

env_passthrough = ["GRAFANA_*"]  # WRONG: becomes runtime.env_passthrough

Place standalone keys before the first [table] header instead.

Initialising config with init

Rather than editing config.toml by hand, use agent-circus init flags to write common options directly. This creates .agent-circus/config.toml (and the directory, if absent) without copying any template files — instant mode continues to work alongside a config.toml.

# Enable SSH agent forwarding
agent-circus init --ssh

# SSH forwarding with config and known_hosts passthrough
agent-circus init --ssh \
  --ssh-config ~/.ssh/config \
  --ssh-known-hosts ~/.ssh/known_hosts

# Shadow secret files
agent-circus init --shadow .env --shadow .env.local

# Git config with commit signing
agent-circus init --git --git-signing-key ~/.ssh/id_ed25519.pub

# Combine with full deploy
agent-circus init --deploy --ssh --git --shadow .env

All flags are additive and idempotent: running them again merges into the existing config.toml without overwriting unrelated keys.

FlagDescription
--sshAdd [ssh] table to enable SSH agent forwarding
--ssh-config PATHSet ssh.config_path (implies --ssh)
--ssh-known-hosts PATHSet ssh.known_hosts_path (implies --ssh)
--shadow TEXTAppend a path to the shadow list (repeatable)
--gitAdd [git] table to enable Git config forwarding
--git-config PATHSet git.config_path (implies --git)
--git-signing-key PATHSet git.signing_key_path (implies --git)
--hosts-pattern TEXTAppend a pattern to hosts.patterns (repeatable)
--ca-cert-pattern TEXTAppend a pattern to ca_certs.patterns (repeatable)
--env-pattern TEXTAppend a pattern to env_passthrough (repeatable)
--git-worktree-mirrorSet git.worktree_mirror = true and write AGENTS.md

Container Runtimes

Docker and Podman are supported container runtimes. Docker is the default. Podman supports rootless workflows and uses podman compose, which delegates to an installed Compose provider such as podman-compose or Docker Compose.

Select the runtime with, in priority order:

  1. CLI option:
    agent-circus up --runtime podman
    agent-circus build --runtime docker
    
  2. Environment variable:
    AGENT_CIRCUS_RUNTIME=podman agent-circus up
    
  3. Config file:
    [runtime]
    engine = "podman"
    
  4. Default:
    docker
    

The CLI option always wins over the environment. For example, this uses Docker:

AGENT_CIRCUS_RUNTIME=podman agent-circus up --runtime docker

Because Podman's Compose command delegates to an external provider, exact Compose behavior can depend on which provider and version are installed. Agent Circus logs runtime and provider version details at debug level to help diagnose provider-specific behavior.

For Agent Circus-managed writable state, such as project-local agent config stores and data stores, Podman services use userns_mode: keep-id so rootless containers can access host-owned state directories without recursively changing host ownership with Podman's U volume option. Direct --host-config mounts use the same Podman user namespace behavior, but still point at the real host provider configuration directory.

Shadowing Files

The shadow setting prevents host files from leaking into containers by overlaying them with /dev/null bind mounts:

shadow = [".env", ".env.local"]

This is useful for keeping API keys and other secrets in .env files out of agent containers.

MCP Servers

MCP servers run as sidecar containers. Agent Circus starts them automatically and injects the server URLs into every agent's native configuration.

Add entries to config.toml:

[[mcp_servers]]
name = "filesystem"
image = "mcp/filesystem:latest"

Optional fields: port (default 8080), transport (default streamable-http), path (default /mcp), env, command, volumes, and install_ca_certs (default false).

Configured [hosts] entries and [ca_certs] files are also forwarded to MCP sidecars. Set install_ca_certs = true for an MCP image that provides /bin/sh, cp, and update-ca-certificates; Agent Circus then installs the mounted certificates as root using a Docker Compose post_start hook. Leave it disabled for images that use another trust-store mechanism.

Check running sidecars with agent-circus ps --mcp.

To use an MCP server that is already running, configure its network URL instead of an image:

[[mcp_servers]]
name = "existing"
url = "http://host.docker.internal:9000/mcp"
transport = "streamable-http"

Each entry must define exactly one of image or url. URL-based entries only inject agent configuration; Agent Circus does not create, start, stop, or show a sidecar for them. On Linux, host.docker.internal is mapped through Docker's host-gateway. The host server must listen on an address reachable from Docker, such as 0.0.0.0; a service bound only to host 127.0.0.1 is generally not reachable from a bridge-network container.

External MCP servers currently support unauthenticated HTTP or HTTPS access only. Authentication headers, tokens, OAuth, client certificates, and access to an existing host stdio process are not supported.

To run an MCP server as a local stdio child process inside each agent container, install its executable in the agent image and configure it without image or url:

env_passthrough = ["re:^GITHUB_"]

[[mcp_servers]]
name = "github"
transport = "stdio"
command = "github-mcp-server"
args = ["stdio"]
env_vars = ["GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_HOST"]

Install the GitHub MCP server in every agent image with a base_root build hook. This example installs version 1.2.0 for x86-64 agent images:

[hooks]
base_root = """
curl -fsSL \
  https://github.com/github/github-mcp-server/releases/download/v1.2.0/github-mcp-server_Linux_x86_64.tar.gz \
  | tar -xz -C /usr/local/bin github-mcp-server
chmod 0755 /usr/local/bin/github-mcp-server
"""

If the configuration already defines hooks.base_root, append these commands to that hook instead of adding a second [hooks] table. Rebuild the agent images after changing a build hook.

Stdio entries require command; args and env_vars are optional lists of strings. They do not create Compose sidecars. The command must be available in every agent image, for example through a build hook.

Environment forwarding has two steps: env_passthrough makes variables from the host available in the agent container, while env_vars tells MCP clients such as Codex which variable names may be forwarded to the stdio child process. Only the names are written to generated agent configuration; secret values remain in the environment.

Local LLM via llama.cpp (OpenCode)

Note: Experimental feature; might change when implementing a future plugin mechanism.

Add a [llama_cpp] table to config.toml to run a llama.cpp server as a sidecar container and wire it into OpenCode as a local provider. When enabled, Agent Circus starts the sidecar automatically alongside the opencode container and injects the provider configuration into opencode.json so OpenCode uses the local model without any manual setup.

[llama_cpp]
# All fields are optional — defaults shown below
model = "ggml-org/gemma-3-1b-it-GGUF/gemma-3-1b-it-Q4_K_M.gguf"
models_cache = "${HOME}/.cache/huggingface"
context_size = 2048
extra_args = []

Fields:

FieldRequiredDefaultDescription
modelnoggml-org/gemma-3-1b-it-GGUF/gemma-3-1b-it-Q4_K_M.ggufModel passed to llama-server -m. Accepts a HuggingFace path (owner/repo/file.gguf) or a local path inside the container.
models_cacheno${HOME}/.cache/huggingfaceHost directory bind-mounted into the sidecar so downloaded models survive restarts. Use an absolute path or Compose environment interpolation (${HOME}/…); ~ is not supported.
context_sizeno2048Context window size passed to --ctx-size.
extra_argsno[]List of additional raw flags appended to the llama-server command.

When [llama_cpp] is present, Agent Circus:

  1. Starts a llama-cpp sidecar using ghcr.io/ggml-org/llama.cpp:server.
  2. Bind-mounts models_cache to /models inside the sidecar and sets HF_HOME=/models so the HuggingFace model cache is preserved across restarts — models are only downloaded on first use.
  3. Adds depends_on: llama-cpp to the opencode service so the sidecar starts first.
  4. Merges a provider block into opencode.json pointing OpenCode at http://llama-cpp:8080/v1.

The model identifier used inside OpenCode is derived from the filename stem of model — for example, gemma-3-1b-it-Q4_K_M.gguf becomes gemma-3-1b-it-Q4_K_M.

CPU-only. The ghcr.io/ggml-org/llama.cpp:server image runs on CPU. GPU-enabled images (e.g. server-cuda) require additional Docker configuration and are not currently supported by Agent Circus.

OpenCode only. The llama.cpp sidecar is started unconditionally when [llama_cpp] is present, but the provider injection only targets the opencode service. Other agents are unaffected.

Claude-Mem (Claude Code)

Note: Experimental feature; might change when implementing a future plugin mechanism.

Add a [claude_mem] table to config.toml to enable Claude-Mem for Claude Code. Claude-Mem captures observations from Claude Code sessions and makes project knowledge available to future sessions.

[claude_mem]
enabled = true

When enabled, Agent Circus:

  1. Mounts a workspace-scoped Claude-Mem data directory at /home/node/.claude-mem.
  2. Sets CLAUDE_MEM_DATA_DIR=/home/node/.claude-mem for the claude-code service.
  3. Runs Claude-Mem setup idempotently when the claude-code container starts.
  4. Starts the Claude-Mem worker if it is not already running.

Claude-Mem data is stored under ~/.local/state/agent-circus/<project>/data/claude-mem/ on the host. Each workspace gets its own memory store, so knowledge from different projects or customers is not mixed. Disabling [claude_mem] stops mounting and starting Claude-Mem, but does not delete the existing memory data.

Fields:

FieldRequiredDefaultDescription
enablednofalseEnable Claude-Mem for supported services
scopenoworkspaceMemory scope; currently only workspace is supported
servicesno["claude-code"]Services that receive Claude-Mem; currently only claude-code is supported

Claude-Mem uses its own default provider configuration. If you configure provider API keys with Claude-Mem, forward them with env_passthrough so secret values remain outside Agent Circus config files.

env_passthrough = ["CLAUDE_MEM_GEMINI_API_KEY", "CLAUDE_MEM_OPENROUTER_API_KEY"]

Claude Code only. Claude-Mem support currently targets the claude-code service. Other supported Claude-Mem IDEs and CLIs are not wired up by Agent Circus yet.

Rebuild required: Claude-Mem and Bun are installed in the claude-code image. Run agent-circus build claude-code after enabling [claude_mem] for the first time or after updating Agent Circus.

Host config isolation: agent-circus exec claude-code --host-config is rejected while [claude_mem].enabled = true, because Claude-Mem setup writes Claude Code plugin files and should only modify the workspace-local config store.

Additional Directories

Use the [[additional_dirs]] array to mount extra host directories into every agent container. Directories appear under /workspaces/<name> alongside the primary project at /workspace.

[[additional_dirs]]
path = "/home/user/shared-libs"
readonly = true

[[additional_dirs]]
path = "/home/user/other-project"
# readonly defaults to false
# name defaults to the basename of path
name = "other-project"

Fields:

FieldRequiredDefaultDescription
pathyesAbsolute path on the host
readonlynofalseMount read-only when true
namenobasename of pathContainer mount name (/workspaces/<name>)

Data Stores

Use the [[data_stores]] array to persist data across container restarts on a per-project basis. Each named store is a directory kept under ~/.local/state/agent-circus/<project>/data/<name>/ on the host and bind-mounted into configured agent containers.

[[data_stores]]
name = "memory"
# mount_path defaults to /home/node/.local/share/agent-circus/<name>

[[data_stores]]
name = "bashhistory"
mount_path = "/commandhistory"

Fields:

FieldRequiredDefaultDescription
nameyesStore name; used as the host subdirectory
mount_pathno/home/node/.local/share/agent-circus/<name>Container mount path
servicesnoall agent servicesServices that receive the store
seed_fromnounsetHost directory copied into the store before first container start; use an absolute path or environment interpolation like ${HOME}/.codex
seed_modenoonceSeed copy behavior; currently only once is supported

Seeded stores are useful when you want a writable, project-local copy of other host state. Tilde paths such as ~/.local/share/tool are rejected; use an absolute path or environment interpolation such as ${HOME}/.local/share/tool.

Seeding currently happens only once per data store. After the first successful seed copy, later container starts keep the data store's existing contents and do not re-copy from seed_from. Optional always-on seeding will be added in a future release. To re-initiate seeding for a user-defined data store, remove the store directory (~/.local/state/agent-circus/<project>/data/<name>/) before starting the container again. If you want to keep the existing store contents and only allow another seed copy into it, remove the store's .agent-circus-seeded marker file instead.

Bash history. The container image configures shells to write history to /commandhistory. Adding a bashhistory data store (as shown above) makes that history survive container restarts. Without a data store, history is kept only for the lifetime of the container.

Port Forwarding

Use the [[port_forwards]] array to publish a container port on the host. Each entry applies to one explicit agent service.

[[port_forwards]]
service = "codex"
container_port = 3333
host_port = 3333
# host defaults to 127.0.0.1
# protocol defaults to tcp

For example, to reach the lean-ctx dashboard from the host, publish port 3333 and run the dashboard inside the container bound to the container network interface:

lean-ctx dashboard --host=0.0.0.0 --port=3333

The process inside the container must bind to 0.0.0.0 or the container network interface. If it binds only to 127.0.0.1 inside the container, Docker port publishing cannot reach it from the host.

Fields:

FieldRequiredDefaultDescription
serviceyes-Agent service to expose
container_portyes-Port inside the container
host_portnocontainer_portPort on the host
hostno127.0.0.1Host bind address
protocolnotcptcp or udp

Agent Circus binds forwarded ports to 127.0.0.1 by default. Use host = "0.0.0.0" only when the UI or API should be reachable from other machines on the network.

Published ports are set when Docker creates the container. If port forwarding configuration changes for an already-created service, recreate the container:

agent-circus remove
agent-circus up codex

Host port collisions are left to Docker Compose. If the requested host port is already in use, Docker reports its normal error. Agent Circus only publishes the port; it does not check whether a process is listening inside the container.

SSH Agent Forwarding

Add an [ssh] table to config.toml to forward your host SSH agent into agent containers. This lets agents interact with Git servers (e.g. GitHub) over SSH without any key material entering the container — all private key operations stay on the host.

[ssh]

The host's SSH agent socket ($SSH_AUTH_SOCK) is mounted read-only at /run/ssh-agent.sock inside each container, and SSH_AUTH_SOCK is set accordingly.

Note: SSH_AUTH_SOCK must be set in your environment when running agent-circus. If it is not, agent-circus will report an error.

Starting an SSH agent on the host

Linux — desktop session (GNOME, KDE, etc.)

Most desktop environments start an SSH agent automatically as part of the session. SSH_AUTH_SOCK is usually already set; verify with:

echo $SSH_AUTH_SOCK

Linux — systemd user service

For headless or server setups, enable the systemd SSH agent user service:

systemctl --user enable --now ssh-agent

Add the following to your shell profile (e.g. ~/.bashrc or ~/.zshrc) to make the socket available in new shells:

export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"

Linux — manual (one-off)

Start a temporary agent in your current shell session:

eval $(ssh-agent)

macOS

macOS integrates the system keychain as an SSH agent; SSH_AUTH_SOCK is set automatically in GUI and terminal sessions. No extra setup is needed.

Loading keys into the agent

Once the agent is running, add your key(s):

ssh-add ~/.ssh/id_ed25519
# or to add all default keys:
ssh-add

On macOS you can persist keys across reboots in the keychain:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Passing SSH config and known_hosts

By default only the agent socket is forwarded. To also make your SSH config and known hosts available inside containers, add the optional fields:

[ssh]
config_path = "~/.ssh/config"
known_hosts_path = "~/.ssh/known_hosts"

Both fields are optional and independent of each other. Tilde (~) is expanded to your home directory. The files are mounted read-only at intermediate paths and copied into /home/node/.ssh/ at container startup with correct ownership — no key material is involved.

Rebuild required: The copy logic lives in docker-entrypoint.sh which is baked into the image. Run agent-circus build after enabling these options for the first time.

IdentityFile directives: If your ~/.ssh/config references local key paths (e.g. IdentityFile ~/.ssh/id_ed25519), those paths won't exist inside the container. This is harmless — SSH falls back to the forwarded agent automatically when a referenced file is absent.

Git Configuration

Add a [git] table to config.toml to forward your host Git configuration into agent containers, giving agents the correct identity for commits and enabling SSH commit signing.

[git]
config_path = "~/.gitconfig"              # optional, defaults to ~/.gitconfig
signing_key_path = "~/.ssh/id_ed25519.pub"  # optional
worktree_mirror = true                    # optional, see below

The host gitconfig is mounted read-only at /run/git-host/config. At container startup the entrypoint generates /home/node/.gitconfig using git's native [include] mechanism:

[include]
    path = /run/git-host/config
[user]
    signingkey = /run/git-host/signingkey.pub   # only when signing_key_path is set

All portable settings (name, email, aliases, …) flow through the include unchanged. Only user.signingkey is overridden to point to the container-local path, resolving the host-absolute-path issue without rewriting the original file. The public key is not copied into ~/.ssh; when signing_key_path is set it is mounted at /run/git-host/signingkey.pub.

SSH commit signing still uses your private key through the forwarded SSH agent. Load the matching private key on the host before starting or using the container, e.g.:

ssh-add ~/.ssh/id_ed25519

Inside the container, ssh-add -l should list the key and git config --global --get user.signingkey should print /run/git-host/signingkey.pub.

Rebuild required: The entrypoint logic is baked into the image. Run agent-circus build after enabling [git] for the first time.

[include] directives in your gitconfig that reference other host paths (e.g. ~/.gitconfig.local) will silently produce no-ops inside the container. Extract the portable parts into the forwarded file or use config_path to point at a dedicated container-friendly config.

Worktree support (worktree_mirror)

Set worktree_mirror = true when the repository is also used outside the containers and you need transparent Git worktree support.

This adds a second bind mount for the workspace at its exact host absolute path inside every agent container (in addition to the standard /workspace mount). Both paths point to the same directory, so Git's recorded absolute paths are valid inside the container — git worktree list, git worktree add, and all other operations that consume or emit absolute paths work without any manual path rewriting.

Enable it via agent-circus init:

agent-circus init --git-worktree-mirror

This also writes a managed ## Git Operations section to AGENTS.md in the workspace, instructing agents to use the host path (e.g. /home/user/projects/myrepo) rather than /workspace for Git operations. The section is bounded by HTML comment markers and updated idempotently on subsequent runs.

Note: The host path of the workspace is embedded in AGENTS.md at init time. Re-run agent-circus init --git-worktree-mirror if the repository is moved to a different location on the host.

CA Certificates

Add a [ca_certs] table to config.toml to forward selected CA certificates from the host into agent containers. This is needed when agents make HTTPS requests to internal services signed by a private or corporate CA.

[ca_certs]
patterns = ["corp-*.crt", "my-vpn-ca.crt", "re:internal"]
# path = "/usr/local/share/ca-certificates"   # optional, defaults as above

Certificates whose filename matches a pattern are bind-mounted at /run/ca-host/<filename> and installed into the container's system trust store by the entrypoint (update-ca-certificates), making them trusted for all tools (curl, git, npm, etc.).

Pattern syntax is the same as [hosts]: fnmatch glob by default, re: prefix for regex.

Use agent-circus init to write patterns:

agent-circus init --ca-cert-pattern "corp-*.crt" --ca-cert-pattern "vpn-ca.crt"

Rebuild required: install-ca-certs.sh and its sudoers rule are baked into the image. Run agent-circus build after enabling [ca_certs] for the first time.

Host Entries

Add a [hosts] table to config.toml to forward selected entries from the host's /etc/hosts into agent containers. This is useful for reaching internal servers, VPN targets, or other hosts that are only resolvable on the host machine.

[hosts]
patterns = ["*.corp.internal", "myserver", "re:\\.vpn\\.example\\.com$"]
# file = "/etc/hosts"   # optional, defaults to /etc/hosts

Matching entries are injected via Docker Compose's extra_hosts mechanism, which adds them to /etc/hosts inside every agent container.

Pattern syntax:

PatternSyntaxExample
*.corp.internalfnmatch glob (default)matches foo.corp.internal
myserverexact globmatches only myserver
re:\.vpn\.Python regex (prefix re:)matches any name containing .vpn.
  • Matching is case-insensitive.
  • If any name on a hosts line matches, all names from that line are forwarded (e.g. if myserver.corp.internal matches, myserver is also forwarded — preserving alias semantics).

Use agent-circus init to write patterns without editing config.toml directly:

agent-circus init --hosts-pattern "*.corp.internal" --hosts-pattern "myserver"

The flag is additive and idempotent: running it again merges new patterns without duplicating existing ones.

Environment Variables

Two complementary mechanisms control environment variables in containers:

MechanismWhenUse for
[env] tableBuild time — baked into the image as ENV instructionsStable toolchain paths, non-secret config
env_passthrough listRuntime — forwarded from the host when the container startsSecrets, tokens, values that change per session

[env] — bake into the image

Each entry is injected as a Docker ENV instruction at the end of the base build stage, so $VARNAME is expanded relative to the image's current value — not the host shell.

[env]
GOPATH = "/home/node/go"
PATH = "/usr/local/go/bin:$PATH"

This only affects instant mode. In deploy mode add ENV lines directly to .agent-circus/Dockerfile.

env_passthrough — forward from the host at runtime

Variable values are never stored in any config or override file — Docker Compose reads them directly from the host environment when the container starts.

env_passthrough = ["MY_CORP_*", "re:^VAULT_", "ANTHROPIC_API_KEY"]

Pattern syntax is the same as [hosts] and [ca_certs]: fnmatch glob by default, re: prefix for regex, case-insensitive.

agent-circus init --env-pattern "MY_CORP_*" --env-pattern "re:^VAULT_"

Combined example

A typical Go project that needs a corporate proxy and a rotating API token:

# Bake the Go toolchain path into the image once (stable, non-secret).
[env]
GOPATH = "/home/node/go"
PATH = "/usr/local/go/bin:$PATH"
GOPROXY = "https://proxy.corp.internal/go,direct"

# Forward secrets and session-specific values from the host at runtime.
env_passthrough = ["ANTHROPIC_API_KEY", "MY_CORP_*", "re:^VAULT_"]

[logging] — log level and file

Control logging from your user-global config.toml (~/.config/agent-circus/config.toml):

[logging]
level = "DEBUG"                      # DEBUG | INFO | WARNING | ERROR | CRITICAL
file  = "/tmp/agent-circus.log"      # optional; omit to log to stdout only

The --log-level and --log-file CLI flags (and their LOGLEVEL / LOGFILE environment variables) take precedence over config.toml. This section is read from the user-global config only — project-local logging config is not supported because logging is initialised before the workspace is known.

Hooks

Hooks let you inject custom shell commands into the Docker image build, without modifying the shared Dockerfile.

ScriptRuns asWhenTypical use
base-root.shrootBuild timeInstall apt packages, system-level configuration
base-user.shnodeBuild timeInstall npm/uv/pip packages, user-level tooling
startup.shnodeContainer startProject-specific runtime setup

Build scripts are optional and removed from the image after execution. startup.sh is read directly from the bind-mounted workspace at container start — changes take effect immediately without a rebuild.

Inline hooks via config.toml (instant mode)

In instant mode you can define hook content directly in config.toml using the [hooks] table. This works without a .agent-circus/ directory and is ideal for user-global hooks that apply to every project:

[hooks]
base_root = """
apt-get update && apt-get install -y ripgrep fd-find
"""

base_user = """
npm install -g @myorg/custom-tool
curl -fsSL https://my-tool.sh | sh
"""

startup = """
uv sync
"""

Values are multi-line TOML strings written verbatim as shell scripts. A #!/usr/bin/env bash shebang is prepended automatically when absent. If both a [hooks] table and project hook files exist, config.toml wins.

Note: startup.sh defined in config.toml is written to the XDG state directory and bind-mounted into the container at /workspace/.agent-circus/hooks/startup.sh. It takes precedence over any startup.sh file in the workspace. base_root and base_user are ignored in deploy mode — place those scripts in .agent-circus/hooks/ directly.

Hook files (deploy mode)

In deploy mode, place scripts under .agent-circus/hooks/:

# .agent-circus/hooks/base-root.sh
apt-get update \
    && apt-get install -y ripgrep
# .agent-circus/hooks/base-user.sh
npm install -g @myorg/custom-tool
# .agent-circus/hooks/startup.sh
# Sync project dependencies and start an SSH tunnel to an internal host.
uv sync
autossh -M 0 -f -N -L 5432:db.internal:5432 tunnel-user@bastion.internal

Setting up Editors to Work with ACP

Emacs

This is a working agent-shell configuration based agent-circus:

(defconst rpo/agent-shell--container-workspace-path "/workspace/"
  "The workspace path inside agent containers.")

(defun rpo/agent-shell--resolve-container-path (path)
  "Resolve PATH between local filesystem and container workspace.

For example:

- /workspace/README.md
    => /home/xenodium/projects/kitchen-sink/README.md
- /home/xenodium/projects/kitchen-sink/README.md
    => /workspace/README.md"
  (let ((cwd (agent-shell-cwd)))
    (if (string-prefix-p cwd path)
        ;; Local -> container
        (string-replace cwd rpo/agent-shell--container-workspace-path path)
      ;; Container -> local
      (if agent-shell-text-file-capabilities
          (if-let* ((_ (string-prefix-p rpo/agent-shell--container-workspace-path path))
                    (local-path (expand-file-name
                                 (string-replace rpo/agent-shell--container-workspace-path cwd path))))
              (or
               (and (file-in-directory-p local-path cwd) local-path)
               (error "Resolves to path outside of working directory: %s" path))
            (error "Unexpected path outside of workspace folder: %s" path))
        (error "Refuse to resolve to local filesystem with text file capabilities disabled: %s" path)))))

(defun rpo/agent-shell-circus-runner-multi (buffer)
  "Return the agent-circus exec command prefix to run for BUFFER's agent.

Looks up the agent identifier in BUFFER's `agent-shell' config and
selects the matching `agent-circus exec` service, defaulting to
\"claude-code\" when no identifier-specific override is found.

Works in both instant mode and deploy mode."
  (let* ((cfg (agent-shell-get-config buffer))
         (id  (map-elt cfg :identifier))
         (service
          (pcase id
            ('claude-code "claude-code")
            ('codex "codex")
            ('mistral-vibe "mistral-vibe")
            ('opencode "opencode")
            (_ "claude-code"))))
    (list "agent-circus" "--log-level" "DEBUG" "--log-file" "/tmp/agent-circus.log" "exec" service "--")))

(use-package agent-shell
  :ensure t
  :config
  (setq agent-shell-mistral-authentication
        (agent-shell-mistral-make-authentication :api-key "ignored"))
  (setq acp-logging-enabled t)
  (setq agent-shell-command-prefix #'rpo/agent-shell-circus-runner-multi)
  (setq agent-shell-path-resolver-function #'rpo/agent-shell--resolve-container-path)
  (setq agent-shell-file-completion-enabled t))

Roadmap

Planned areas of work include:

  • Expanded Docker and Podman compatibility testing across runtime and Compose provider versions, plus continued hardening for additional container engines.
  • A plugin mechanism that keeps core isolation features small and focused while making integrations easier to add, share, and select per project.

Plugin Mechanism

The plugin infrastructure is intended to separate core mechanisms such as filesystem and network isolation from optional components. New agent harnesses, sidecars, and integrations should be addable without changing the core, and add-ons such as llama.cpp or Claude-Mem should become easier to package, share, and enable only where they are needed, e.g. per-project.

Disclosure: AI-Assisted Development

This project is in large parts AI-generated code. However, humans manage the architecture and perform code reviews of those parts which are generated by AI.

Furthermore, this is a self-hosting project. Agents working on this project are running inside what agent-circus provides. To reduce model risk, AI systems from different vendors are assigned distinct roles in the development process (implementation, testing, and security review).

Contributors

rpoisel

220 commits

Embedded-Focus/agent-circus

Run AI coding agents in sandboxed containers communicate via ACP

32

stars

225

commits

Python

primary language

Sep 14, 2026

updated

embedded-focus.com/blog/agent-circus/
ai-agents
containerization
security

README

AI Agents Circus

AI Agents Circus

Run AI coding agents in isolated Docker or Podman containers with one config language.

License: MIT CI Security Dependabot Python 3.11+ Docker Podman uv Last commit Agents: Claude Code · Codex · Vibe CLI · OpenCode

Open Protocols · Getting Started · Working with the Environment · Authentication · Configuration · Container Runtimes · Hooks · Editors · Roadmap

Run AI coding agents in sandboxed containers with full control over what they can see, reach, and inherit from your host environment.

Docker and Podman are supported container runtimes. Docker is the default; Podman, including rootless operation, can be selected from the CLI, environment, or project configuration.

Agent Circus wraps each agent in its own container, giving you a reproducible, isolated environment that works across machines and projects. You decide which files agents can access, secrets stay on the host, and a built-in firewall restricts outbound network access to known-good destinations.

It also provides a universal configuration mechanism for operating agent harnesses consistently. Authentication, Git identity, SSH agent forwarding, MCP servers, environment variables, extra mounts, hooks, ports, and data stores are declared once in project config and applied across supported agents.

Getting started takes two commands. No files are written to your project, no manual container setup required. Customize per-project only when you need to.

Currently supported agents:

IDEs interface with agents via the Agent Client Protocol (ACP).

Open Protocols

Agent Circus is built around open interfaces instead of editor- or vendor-specific integrations:

Open protocols map showing ACP and MCP relationships

Getting Started

Installing the agent-circus Tool

The host CLI requires Python 3.11 or newer and is tested on Python 3.11–3.14. The agent containers manage their own runtimes; the Mistral Vibe image uses Python 3.14 independently of the Python running the host CLI.

Install the published package from PyPI with uv:

uv tool install agent-circus

It is also possible to use agent-circus without installing it explicitly. For example:

uvx agent-circus version

Alternatively, install it into a Python 3.11 or newer virtual environment using pip:

python3.11 -m venv agent-circus-venv
. agent-circus-venv/bin/activate
python -m pip install agent-circus

The agent-circus command is available while this virtual environment is activated. Activate it again in a new shell before using the tool. On Linux, the venv module may be provided as a separate package by your distribution.

You can use agent-circus from any project directory; cloning this repository is not required. Docker or Podman must be installed on the host to build and run the agent containers.

To install from a source checkout for development instead, run this command in the repository root:

uv tool install .

Version Information

The version command includes the full Git commit. For an installation from a local checkout, it reports that checkout's current HEAD. Distribution artifacts, including packages installed from PyPI, contain the commit recorded when they were built.

Shell Completions

agent-circus completion <shell> prints a completion script for bash, zsh, fish, powershell, or pwsh (defaults to bash), following the <cmd> completion <shell> convention used by tools like kubectl and gh. Source it directly in your shell startup file:

source <(agent-circus completion bash)

Alternatively, for bash, agent-circus completion bash --install writes the script to ~/.bash_completion.d/agent-circus, a directory many bash-completion setups auto-source, creating it if missing and leaving it untouched if already up to date:

agent-circus completion bash --install

If you use a shorter alias for agent-circus (e.g. alias ac=agent-circus), pass --alias (bash only) to also define the alias and wire up its completion in the same script:

agent-circus completion bash --install --alias ac

See the uv tool documentation on how to work with tools in general.

After installing you can start right away in one of your projects (instant mode):

# Docker (default)
agent-circus build
agent-circus exec claude-code -- claude-agent-acp

# Podman
agent-circus build --runtime podman
agent-circus exec --runtime podman claude-code -- claude-agent-acp

The exec command automatically starts the container if it is not running yet. There is no separate up step needed.

Agent Circus is not tied to ACP or editor integrations: the command after -- can also be a direct agent CLI such as codex, or a shell such as bash, when you want to work inside the harness container.

Note: Auto-started containers are not automatically removed when they are no longer in use. Run agent-circus remove to clean up idle containers.

Upgrading and Uninstalling

For an installation managed by uv, upgrade or uninstall the tool with:

uv tool upgrade agent-circus
uv tool uninstall agent-circus

For an installation in an activated virtual environment, use:

python -m pip install --upgrade agent-circus
python -m pip uninstall agent-circus

Working with the Environment

There are two modes of operation:

Instant Mode (Preferred)

Instant mode uses the templates bundled in the agent-circus package directly. No files are written to your project directory. Just run commands against any workspace:

# Build container images
agent-circus build

# Execute a command in a container (starts it automatically)
agent-circus exec claude-code -- claude-agent-acp
agent-circus exec codex -- codex-acp
agent-circus exec opencode -- opencode
agent-circus exec -T claude-code -- echo hello   # non-interactive

# Optionally start containers ahead of time
agent-circus up                           # start all services
agent-circus up claude-code               # start a single service

# Show status of agent containers
agent-circus ps

# Remove all related resources
agent-circus remove
agent-circus remove --volumes             # also remove named volumes
agent-circus remove --force               # don't ask for permission

Updating container dependencies

Each Agent Circus release includes a baseline set of agent and tool versions. You can select newer upstream versions without upgrading the CLI:

agent-circus deps show
agent-circus deps update --dry-run
agent-circus deps update
agent-circus build
agent-circus up

deps update updates the complete supported dependency set: npm, uv, yq, Claude Code, Claude Agent ACP, Bun, Claude-Mem, Codex, Codex ACP, OpenCode, and Mistral Vibe, including its Python transitive dependencies. It accepts stable versions and does not automatically downgrade existing dependencies. The Node base image and apt packages are outside this updater's scope.

Host requirement: uv

Applying an update requires the uv executable on the host. Install it before using deps update; no particular host Python version needs to be installed:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

The operation also needs network access to GitHub, the Python package registry, and the Python download service. It runs entirely on the host and does not start or execute code inside an agent container. Agent Circus runs the following command in a disposable copy of the effective container recipe:

uv lock --upgrade --prerelease if-necessary --python 3.14 --no-config

Mistral Vibe supports Python 3.12 and newer. Agent Circus deliberately runs it on Python 3.14 to use the current Python runtime in the controlled container environment. For lock resolution, uv uses an existing Python 3.14 interpreter or automatically downloads a managed interpreter to its host cache. This does not require a system-wide Python installation or manual preparation. Automatic setup can fail if downloads are disabled, for example with UV_PYTHON_DOWNLOADS=never, or if the download service cannot be reached. To diagnose that configuration directly, run:

uv python find 3.14

The command only generates a lockfile; it does not create or synchronize a host project environment. The candidate recipe is temporary, and the active dependency selection changes only after every lookup, lock, and validation step succeeds. Direct tool versions must be stable. Python transitive dependencies may use a prerelease when it is required by a selected stable package; Mistral Vibe currently has such a dependency. --dry-run only checks direct tool versions; it does not prepare Python 3.14 or resolve Python transitive dependency changes.

If uv fails, Agent Circus prints its captured output after removing likely credentials. To retain diagnostics from a retry, put the global logging options before the subcommand:

agent-circus --log-file /tmp/agent-circus.log deps update

There is no persistent per-operation log unless --log-file, LOGFILE, or the user-global logging configuration is set. Output from an earlier invocation that did not use one of those options cannot be recovered.

Updates take effect only after rebuilding images and recreating containers. Run agent-circus up after build to let Compose recreate containers whose images changed. Existing running containers are not changed by deps update. You can also use agent-circus up --build for the build/start steps.

Authentication

GitHub discovery uses GITHUB_TOKEN, falling back to GH_TOKEN, or anonymous access if neither is set. For example, with a token already set in your shell:

agent-circus deps update --dry-run

The token needs access to public release metadata; no repository write access is needed. Authentication and API rate-limit errors leave the active dependency selection unchanged. Tokens are not saved in snapshots or added to container build arguments. npm and PyPI discovery use public registries; private registry configuration and automatic reuse of gh auth credentials are not supported by this feature.

Selection scope and history

In instant mode, selections live under $XDG_DATA_HOME/agent-circus/dependencies/, defaulting to ~/.local/share/agent-circus/dependencies/. An update affects subsequent builds in all instant-mode workspaces for that user.

In deploy mode, selections live in .agent-circus/dependencies/ in the project and take precedence over user-wide selections. A deployed project with no selection uses the versions in its deployed templates. You can commit its selection.json and snapshots/ directory to share exact selections with your team; exclude the .lock file. All commands accept --workspace PATH (-w PATH) after the subcommand.

agent-circus deps history
agent-circus deps rollback
agent-circus deps reset
agent-circus deps show --workspace /path/to/project

rollback selects the previous snapshot; repeating it switches back to the selection you just left. reset selects the installed CLI release's baseline. Both operations work without upstream access or host uv. Rebuild and run up after either operation to change the containers.

Snapshots contain a version manifest (versions.toml), pyproject.toml, and uv.lock. They are immutable and identified by their content hash. Successful updates save the previous selection, including the bundled baseline, before atomically activating the new snapshot. Failed updates leave the active selection intact; unchanged updates do not duplicate snapshots or discard the rollback target. Snapshots are retained until you manually remove unused ones. Do not edit snapshot contents or remove active/previous snapshots.

deps show distinguishes the release baseline from locally resolved upstream versions. Successful upstream resolution does not establish that the new agents work together. Snapshots preserve versions, not downloaded artifacts or container images: rebuilding still requires upstream artifacts to be available. They also do not lock every npm transitive dependency, apt package, or base-image digest, so they do not guarantee byte-for-byte reproducible images.

CLI upgrades and deployed templates

Instant mode uses a new release's baseline after a CLI upgrade when no explicit selection is active. Explicit selections remain active if their recipe format is compatible; incompatible selections produce an error instead of being silently replaced. Use deps reset to switch to the installed release's baseline. Saved baseline snapshots remain available for rollback when compatible. Deployed projects keep their files and selection across CLI upgrades; use deps reset explicitly to select the new baseline.

Updates never overwrite deployed Dockerfiles or Compose files. Builds use a temporary copy and a build override that preserves the original Compose path base. This first implementation recognizes recipes matching the installed release except for supported version pins. Custom hook scripts and other copied files are preserved; changes to Dockerfile, Compose structure, or Python project configuration require migration before using a dependency selection.

To migrate an unsupported deployment, back up your customizations, redeploy with agent-circus init --deploy --force, and reapply supported customizations through hooks and config.toml. The force option overwrites deployed template files, including hooks, so keep your backup until migration is complete.

Maintainers can continue using make update-templates-dryrun and make update-templates to prepare the next release's bundled baseline. These source-maintenance targets are separate from end-user dependency selections. The manual Dependency container smoke tests GitHub Actions workflow builds both template modes and checks agent version commands against the baseline or newly resolved upstream dependencies; run it before accepting a release baseline.

Deploy Mode

Deploy mode copies configuration files into a .agent-circus/ directory inside your project. Use this if you need to customize the Dockerfile or compose.yaml per project.

# Deploy configuration files to the workspace
agent-circus init --deploy

# All other commands work the same; deploy mode is auto-detected
agent-circus build
agent-circus exec claude-code -- claude-agent-acp

# Remove containers and deployed files
agent-circus remove --destroy             # remove containers + .agent-circus/
agent-circus remove --destroy --force     # don't ask for permission

When both a deployed .agent-circus/ directory and instant mode are available, deploy mode takes priority.

Authentication

Each agent authenticates against its vendor's API using the credentials already present on the host. Agent Circus seeds a private, project-local writable copy of each vendor configuration directory and mounts that copy into the corresponding container:

AgentSeed sourceContainer path
Claude Code~/.claude/home/node/.claude
Codex~/.codex/home/node/.codex
Vibe CLI~/.vibe/home/node/.vibe
OpenCode~/.config/opencode/home/node/.config/opencode

The copies are stored under ~/.local/state/agent-circus/<project>/agent-config/<agent>/ with directory permissions restricted to the current user. They are seeded only once, so container changes do not modify host configuration and later host changes are not copied automatically. Agent Circus merges generated settings such as MCP servers into these writable copies before startup.

To discard a copy and seed it again from the current host configuration on the next start, first stop the affected service and run:

agent-circus config reset codex
agent-circus config reset --all

Use --force to skip the confirmation prompt. Reset refuses to delete a config store while its service is running.

For credential refresh or other explicit edits to the real host provider configuration, run a command with --host-config. This bypasses the project-local copy and mounts the host configuration directory writable for that single service. Agent Circus does not merge generated MCP settings into the host configuration while this mode is active.

agent-circus remove --force codex
agent-circus exec codex --host-config -- codex login
agent-circus remove --force codex
agent-circus config reset codex --force
agent-circus up codex

The second remove is needed because exec starts a stopped service and leaves it running. After config reset, the next normal startup seeds a fresh project-local copy from the updated host configuration.

Configuration

Agent Circus can be configured via TOML files. Settings are resolved in this order (last wins):

  1. User-global$XDG_CONFIG_HOME/agent-circus/config.toml (default: ~/.config/agent-circus/config.toml)
  2. Project-local.agent-circus/config.toml in the workspace

CLI flags override both.

[!IMPORTANT] A key = value line belongs to the last [table] header above it, blank lines or not. A top-level key placed after [runtime], [hooks], [ssh], etc. silently becomes part of that table instead of standing on its own, with no error or warning:

[runtime]
engine = "podman"

env_passthrough = ["GRAFANA_*"]  # WRONG: becomes runtime.env_passthrough

Place standalone keys before the first [table] header instead.

Initialising config with init

Rather than editing config.toml by hand, use agent-circus init flags to write common options directly. This creates .agent-circus/config.toml (and the directory, if absent) without copying any template files — instant mode continues to work alongside a config.toml.

# Enable SSH agent forwarding
agent-circus init --ssh

# SSH forwarding with config and known_hosts passthrough
agent-circus init --ssh \
  --ssh-config ~/.ssh/config \
  --ssh-known-hosts ~/.ssh/known_hosts

# Shadow secret files
agent-circus init --shadow .env --shadow .env.local

# Git config with commit signing
agent-circus init --git --git-signing-key ~/.ssh/id_ed25519.pub

# Combine with full deploy
agent-circus init --deploy --ssh --git --shadow .env

All flags are additive and idempotent: running them again merges into the existing config.toml without overwriting unrelated keys.

FlagDescription
--sshAdd [ssh] table to enable SSH agent forwarding
--ssh-config PATHSet ssh.config_path (implies --ssh)
--ssh-known-hosts PATHSet ssh.known_hosts_path (implies --ssh)
--shadow TEXTAppend a path to the shadow list (repeatable)
--gitAdd [git] table to enable Git config forwarding
--git-config PATHSet git.config_path (implies --git)
--git-signing-key PATHSet git.signing_key_path (implies --git)
--hosts-pattern TEXTAppend a pattern to hosts.patterns (repeatable)
--ca-cert-pattern TEXTAppend a pattern to ca_certs.patterns (repeatable)
--env-pattern TEXTAppend a pattern to env_passthrough (repeatable)
--git-worktree-mirrorSet git.worktree_mirror = true and write AGENTS.md

Container Runtimes

Docker and Podman are supported container runtimes. Docker is the default. Podman supports rootless workflows and uses podman compose, which delegates to an installed Compose provider such as podman-compose or Docker Compose.

Select the runtime with, in priority order:

  1. CLI option:
    agent-circus up --runtime podman
    agent-circus build --runtime docker
    
  2. Environment variable:
    AGENT_CIRCUS_RUNTIME=podman agent-circus up
    
  3. Config file:
    [runtime]
    engine = "podman"
    
  4. Default:
    docker
    

The CLI option always wins over the environment. For example, this uses Docker:

AGENT_CIRCUS_RUNTIME=podman agent-circus up --runtime docker

Because Podman's Compose command delegates to an external provider, exact Compose behavior can depend on which provider and version are installed. Agent Circus logs runtime and provider version details at debug level to help diagnose provider-specific behavior.

For Agent Circus-managed writable state, such as project-local agent config stores and data stores, Podman services use userns_mode: keep-id so rootless containers can access host-owned state directories without recursively changing host ownership with Podman's U volume option. Direct --host-config mounts use the same Podman user namespace behavior, but still point at the real host provider configuration directory.

Shadowing Files

The shadow setting prevents host files from leaking into containers by overlaying them with /dev/null bind mounts:

shadow = [".env", ".env.local"]

This is useful for keeping API keys and other secrets in .env files out of agent containers.

MCP Servers

MCP servers run as sidecar containers. Agent Circus starts them automatically and injects the server URLs into every agent's native configuration.

Add entries to config.toml:

[[mcp_servers]]
name = "filesystem"
image = "mcp/filesystem:latest"

Optional fields: port (default 8080), transport (default streamable-http), path (default /mcp), env, command, volumes, and install_ca_certs (default false).

Configured [hosts] entries and [ca_certs] files are also forwarded to MCP sidecars. Set install_ca_certs = true for an MCP image that provides /bin/sh, cp, and update-ca-certificates; Agent Circus then installs the mounted certificates as root using a Docker Compose post_start hook. Leave it disabled for images that use another trust-store mechanism.

Check running sidecars with agent-circus ps --mcp.

To use an MCP server that is already running, configure its network URL instead of an image:

[[mcp_servers]]
name = "existing"
url = "http://host.docker.internal:9000/mcp"
transport = "streamable-http"

Each entry must define exactly one of image or url. URL-based entries only inject agent configuration; Agent Circus does not create, start, stop, or show a sidecar for them. On Linux, host.docker.internal is mapped through Docker's host-gateway. The host server must listen on an address reachable from Docker, such as 0.0.0.0; a service bound only to host 127.0.0.1 is generally not reachable from a bridge-network container.

External MCP servers currently support unauthenticated HTTP or HTTPS access only. Authentication headers, tokens, OAuth, client certificates, and access to an existing host stdio process are not supported.

To run an MCP server as a local stdio child process inside each agent container, install its executable in the agent image and configure it without image or url:

env_passthrough = ["re:^GITHUB_"]

[[mcp_servers]]
name = "github"
transport = "stdio"
command = "github-mcp-server"
args = ["stdio"]
env_vars = ["GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_HOST"]

Install the GitHub MCP server in every agent image with a base_root build hook. This example installs version 1.2.0 for x86-64 agent images:

[hooks]
base_root = """
curl -fsSL \
  https://github.com/github/github-mcp-server/releases/download/v1.2.0/github-mcp-server_Linux_x86_64.tar.gz \
  | tar -xz -C /usr/local/bin github-mcp-server
chmod 0755 /usr/local/bin/github-mcp-server
"""

If the configuration already defines hooks.base_root, append these commands to that hook instead of adding a second [hooks] table. Rebuild the agent images after changing a build hook.

Stdio entries require command; args and env_vars are optional lists of strings. They do not create Compose sidecars. The command must be available in every agent image, for example through a build hook.

Environment forwarding has two steps: env_passthrough makes variables from the host available in the agent container, while env_vars tells MCP clients such as Codex which variable names may be forwarded to the stdio child process. Only the names are written to generated agent configuration; secret values remain in the environment.

Local LLM via llama.cpp (OpenCode)

Note: Experimental feature; might change when implementing a future plugin mechanism.

Add a [llama_cpp] table to config.toml to run a llama.cpp server as a sidecar container and wire it into OpenCode as a local provider. When enabled, Agent Circus starts the sidecar automatically alongside the opencode container and injects the provider configuration into opencode.json so OpenCode uses the local model without any manual setup.

[llama_cpp]
# All fields are optional — defaults shown below
model = "ggml-org/gemma-3-1b-it-GGUF/gemma-3-1b-it-Q4_K_M.gguf"
models_cache = "${HOME}/.cache/huggingface"
context_size = 2048
extra_args = []

Fields:

FieldRequiredDefaultDescription
modelnoggml-org/gemma-3-1b-it-GGUF/gemma-3-1b-it-Q4_K_M.ggufModel passed to llama-server -m. Accepts a HuggingFace path (owner/repo/file.gguf) or a local path inside the container.
models_cacheno${HOME}/.cache/huggingfaceHost directory bind-mounted into the sidecar so downloaded models survive restarts. Use an absolute path or Compose environment interpolation (${HOME}/…); ~ is not supported.
context_sizeno2048Context window size passed to --ctx-size.
extra_argsno[]List of additional raw flags appended to the llama-server command.

When [llama_cpp] is present, Agent Circus:

  1. Starts a llama-cpp sidecar using ghcr.io/ggml-org/llama.cpp:server.
  2. Bind-mounts models_cache to /models inside the sidecar and sets HF_HOME=/models so the HuggingFace model cache is preserved across restarts — models are only downloaded on first use.
  3. Adds depends_on: llama-cpp to the opencode service so the sidecar starts first.
  4. Merges a provider block into opencode.json pointing OpenCode at http://llama-cpp:8080/v1.

The model identifier used inside OpenCode is derived from the filename stem of model — for example, gemma-3-1b-it-Q4_K_M.gguf becomes gemma-3-1b-it-Q4_K_M.

CPU-only. The ghcr.io/ggml-org/llama.cpp:server image runs on CPU. GPU-enabled images (e.g. server-cuda) require additional Docker configuration and are not currently supported by Agent Circus.

OpenCode only. The llama.cpp sidecar is started unconditionally when [llama_cpp] is present, but the provider injection only targets the opencode service. Other agents are unaffected.

Claude-Mem (Claude Code)

Note: Experimental feature; might change when implementing a future plugin mechanism.

Add a [claude_mem] table to config.toml to enable Claude-Mem for Claude Code. Claude-Mem captures observations from Claude Code sessions and makes project knowledge available to future sessions.

[claude_mem]
enabled = true

When enabled, Agent Circus:

  1. Mounts a workspace-scoped Claude-Mem data directory at /home/node/.claude-mem.
  2. Sets CLAUDE_MEM_DATA_DIR=/home/node/.claude-mem for the claude-code service.
  3. Runs Claude-Mem setup idempotently when the claude-code container starts.
  4. Starts the Claude-Mem worker if it is not already running.

Claude-Mem data is stored under ~/.local/state/agent-circus/<project>/data/claude-mem/ on the host. Each workspace gets its own memory store, so knowledge from different projects or customers is not mixed. Disabling [claude_mem] stops mounting and starting Claude-Mem, but does not delete the existing memory data.

Fields:

FieldRequiredDefaultDescription
enablednofalseEnable Claude-Mem for supported services
scopenoworkspaceMemory scope; currently only workspace is supported
servicesno["claude-code"]Services that receive Claude-Mem; currently only claude-code is supported

Claude-Mem uses its own default provider configuration. If you configure provider API keys with Claude-Mem, forward them with env_passthrough so secret values remain outside Agent Circus config files.

env_passthrough = ["CLAUDE_MEM_GEMINI_API_KEY", "CLAUDE_MEM_OPENROUTER_API_KEY"]

Claude Code only. Claude-Mem support currently targets the claude-code service. Other supported Claude-Mem IDEs and CLIs are not wired up by Agent Circus yet.

Rebuild required: Claude-Mem and Bun are installed in the claude-code image. Run agent-circus build claude-code after enabling [claude_mem] for the first time or after updating Agent Circus.

Host config isolation: agent-circus exec claude-code --host-config is rejected while [claude_mem].enabled = true, because Claude-Mem setup writes Claude Code plugin files and should only modify the workspace-local config store.

Additional Directories

Use the [[additional_dirs]] array to mount extra host directories into every agent container. Directories appear under /workspaces/<name> alongside the primary project at /workspace.

[[additional_dirs]]
path = "/home/user/shared-libs"
readonly = true

[[additional_dirs]]
path = "/home/user/other-project"
# readonly defaults to false
# name defaults to the basename of path
name = "other-project"

Fields:

FieldRequiredDefaultDescription
pathyesAbsolute path on the host
readonlynofalseMount read-only when true
namenobasename of pathContainer mount name (/workspaces/<name>)

Data Stores

Use the [[data_stores]] array to persist data across container restarts on a per-project basis. Each named store is a directory kept under ~/.local/state/agent-circus/<project>/data/<name>/ on the host and bind-mounted into configured agent containers.

[[data_stores]]
name = "memory"
# mount_path defaults to /home/node/.local/share/agent-circus/<name>

[[data_stores]]
name = "bashhistory"
mount_path = "/commandhistory"

Fields:

FieldRequiredDefaultDescription
nameyesStore name; used as the host subdirectory
mount_pathno/home/node/.local/share/agent-circus/<name>Container mount path
servicesnoall agent servicesServices that receive the store
seed_fromnounsetHost directory copied into the store before first container start; use an absolute path or environment interpolation like ${HOME}/.codex
seed_modenoonceSeed copy behavior; currently only once is supported

Seeded stores are useful when you want a writable, project-local copy of other host state. Tilde paths such as ~/.local/share/tool are rejected; use an absolute path or environment interpolation such as ${HOME}/.local/share/tool.

Seeding currently happens only once per data store. After the first successful seed copy, later container starts keep the data store's existing contents and do not re-copy from seed_from. Optional always-on seeding will be added in a future release. To re-initiate seeding for a user-defined data store, remove the store directory (~/.local/state/agent-circus/<project>/data/<name>/) before starting the container again. If you want to keep the existing store contents and only allow another seed copy into it, remove the store's .agent-circus-seeded marker file instead.

Bash history. The container image configures shells to write history to /commandhistory. Adding a bashhistory data store (as shown above) makes that history survive container restarts. Without a data store, history is kept only for the lifetime of the container.

Port Forwarding

Use the [[port_forwards]] array to publish a container port on the host. Each entry applies to one explicit agent service.

[[port_forwards]]
service = "codex"
container_port = 3333
host_port = 3333
# host defaults to 127.0.0.1
# protocol defaults to tcp

For example, to reach the lean-ctx dashboard from the host, publish port 3333 and run the dashboard inside the container bound to the container network interface:

lean-ctx dashboard --host=0.0.0.0 --port=3333

The process inside the container must bind to 0.0.0.0 or the container network interface. If it binds only to 127.0.0.1 inside the container, Docker port publishing cannot reach it from the host.

Fields:

FieldRequiredDefaultDescription
serviceyes-Agent service to expose
container_portyes-Port inside the container
host_portnocontainer_portPort on the host
hostno127.0.0.1Host bind address
protocolnotcptcp or udp

Agent Circus binds forwarded ports to 127.0.0.1 by default. Use host = "0.0.0.0" only when the UI or API should be reachable from other machines on the network.

Published ports are set when Docker creates the container. If port forwarding configuration changes for an already-created service, recreate the container:

agent-circus remove
agent-circus up codex

Host port collisions are left to Docker Compose. If the requested host port is already in use, Docker reports its normal error. Agent Circus only publishes the port; it does not check whether a process is listening inside the container.

SSH Agent Forwarding

Add an [ssh] table to config.toml to forward your host SSH agent into agent containers. This lets agents interact with Git servers (e.g. GitHub) over SSH without any key material entering the container — all private key operations stay on the host.

[ssh]

The host's SSH agent socket ($SSH_AUTH_SOCK) is mounted read-only at /run/ssh-agent.sock inside each container, and SSH_AUTH_SOCK is set accordingly.

Note: SSH_AUTH_SOCK must be set in your environment when running agent-circus. If it is not, agent-circus will report an error.

Starting an SSH agent on the host

Linux — desktop session (GNOME, KDE, etc.)

Most desktop environments start an SSH agent automatically as part of the session. SSH_AUTH_SOCK is usually already set; verify with:

echo $SSH_AUTH_SOCK

Linux — systemd user service

For headless or server setups, enable the systemd SSH agent user service:

systemctl --user enable --now ssh-agent

Add the following to your shell profile (e.g. ~/.bashrc or ~/.zshrc) to make the socket available in new shells:

export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"

Linux — manual (one-off)

Start a temporary agent in your current shell session:

eval $(ssh-agent)

macOS

macOS integrates the system keychain as an SSH agent; SSH_AUTH_SOCK is set automatically in GUI and terminal sessions. No extra setup is needed.

Loading keys into the agent

Once the agent is running, add your key(s):

ssh-add ~/.ssh/id_ed25519
# or to add all default keys:
ssh-add

On macOS you can persist keys across reboots in the keychain:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Passing SSH config and known_hosts

By default only the agent socket is forwarded. To also make your SSH config and known hosts available inside containers, add the optional fields:

[ssh]
config_path = "~/.ssh/config"
known_hosts_path = "~/.ssh/known_hosts"

Both fields are optional and independent of each other. Tilde (~) is expanded to your home directory. The files are mounted read-only at intermediate paths and copied into /home/node/.ssh/ at container startup with correct ownership — no key material is involved.

Rebuild required: The copy logic lives in docker-entrypoint.sh which is baked into the image. Run agent-circus build after enabling these options for the first time.

IdentityFile directives: If your ~/.ssh/config references local key paths (e.g. IdentityFile ~/.ssh/id_ed25519), those paths won't exist inside the container. This is harmless — SSH falls back to the forwarded agent automatically when a referenced file is absent.

Git Configuration

Add a [git] table to config.toml to forward your host Git configuration into agent containers, giving agents the correct identity for commits and enabling SSH commit signing.

[git]
config_path = "~/.gitconfig"              # optional, defaults to ~/.gitconfig
signing_key_path = "~/.ssh/id_ed25519.pub"  # optional
worktree_mirror = true                    # optional, see below

The host gitconfig is mounted read-only at /run/git-host/config. At container startup the entrypoint generates /home/node/.gitconfig using git's native [include] mechanism:

[include]
    path = /run/git-host/config
[user]
    signingkey = /run/git-host/signingkey.pub   # only when signing_key_path is set

All portable settings (name, email, aliases, …) flow through the include unchanged. Only user.signingkey is overridden to point to the container-local path, resolving the host-absolute-path issue without rewriting the original file. The public key is not copied into ~/.ssh; when signing_key_path is set it is mounted at /run/git-host/signingkey.pub.

SSH commit signing still uses your private key through the forwarded SSH agent. Load the matching private key on the host before starting or using the container, e.g.:

ssh-add ~/.ssh/id_ed25519

Inside the container, ssh-add -l should list the key and git config --global --get user.signingkey should print /run/git-host/signingkey.pub.

Rebuild required: The entrypoint logic is baked into the image. Run agent-circus build after enabling [git] for the first time.

[include] directives in your gitconfig that reference other host paths (e.g. ~/.gitconfig.local) will silently produce no-ops inside the container. Extract the portable parts into the forwarded file or use config_path to point at a dedicated container-friendly config.

Worktree support (worktree_mirror)

Set worktree_mirror = true when the repository is also used outside the containers and you need transparent Git worktree support.

This adds a second bind mount for the workspace at its exact host absolute path inside every agent container (in addition to the standard /workspace mount). Both paths point to the same directory, so Git's recorded absolute paths are valid inside the container — git worktree list, git worktree add, and all other operations that consume or emit absolute paths work without any manual path rewriting.

Enable it via agent-circus init:

agent-circus init --git-worktree-mirror

This also writes a managed ## Git Operations section to AGENTS.md in the workspace, instructing agents to use the host path (e.g. /home/user/projects/myrepo) rather than /workspace for Git operations. The section is bounded by HTML comment markers and updated idempotently on subsequent runs.

Note: The host path of the workspace is embedded in AGENTS.md at init time. Re-run agent-circus init --git-worktree-mirror if the repository is moved to a different location on the host.

CA Certificates

Add a [ca_certs] table to config.toml to forward selected CA certificates from the host into agent containers. This is needed when agents make HTTPS requests to internal services signed by a private or corporate CA.

[ca_certs]
patterns = ["corp-*.crt", "my-vpn-ca.crt", "re:internal"]
# path = "/usr/local/share/ca-certificates"   # optional, defaults as above

Certificates whose filename matches a pattern are bind-mounted at /run/ca-host/<filename> and installed into the container's system trust store by the entrypoint (update-ca-certificates), making them trusted for all tools (curl, git, npm, etc.).

Pattern syntax is the same as [hosts]: fnmatch glob by default, re: prefix for regex.

Use agent-circus init to write patterns:

agent-circus init --ca-cert-pattern "corp-*.crt" --ca-cert-pattern "vpn-ca.crt"

Rebuild required: install-ca-certs.sh and its sudoers rule are baked into the image. Run agent-circus build after enabling [ca_certs] for the first time.

Host Entries

Add a [hosts] table to config.toml to forward selected entries from the host's /etc/hosts into agent containers. This is useful for reaching internal servers, VPN targets, or other hosts that are only resolvable on the host machine.

[hosts]
patterns = ["*.corp.internal", "myserver", "re:\\.vpn\\.example\\.com$"]
# file = "/etc/hosts"   # optional, defaults to /etc/hosts

Matching entries are injected via Docker Compose's extra_hosts mechanism, which adds them to /etc/hosts inside every agent container.

Pattern syntax:

PatternSyntaxExample
*.corp.internalfnmatch glob (default)matches foo.corp.internal
myserverexact globmatches only myserver
re:\.vpn\.Python regex (prefix re:)matches any name containing .vpn.
  • Matching is case-insensitive.
  • If any name on a hosts line matches, all names from that line are forwarded (e.g. if myserver.corp.internal matches, myserver is also forwarded — preserving alias semantics).

Use agent-circus init to write patterns without editing config.toml directly:

agent-circus init --hosts-pattern "*.corp.internal" --hosts-pattern "myserver"

The flag is additive and idempotent: running it again merges new patterns without duplicating existing ones.

Environment Variables

Two complementary mechanisms control environment variables in containers:

MechanismWhenUse for
[env] tableBuild time — baked into the image as ENV instructionsStable toolchain paths, non-secret config
env_passthrough listRuntime — forwarded from the host when the container startsSecrets, tokens, values that change per session

[env] — bake into the image

Each entry is injected as a Docker ENV instruction at the end of the base build stage, so $VARNAME is expanded relative to the image's current value — not the host shell.

[env]
GOPATH = "/home/node/go"
PATH = "/usr/local/go/bin:$PATH"

This only affects instant mode. In deploy mode add ENV lines directly to .agent-circus/Dockerfile.

env_passthrough — forward from the host at runtime

Variable values are never stored in any config or override file — Docker Compose reads them directly from the host environment when the container starts.

env_passthrough = ["MY_CORP_*", "re:^VAULT_", "ANTHROPIC_API_KEY"]

Pattern syntax is the same as [hosts] and [ca_certs]: fnmatch glob by default, re: prefix for regex, case-insensitive.

agent-circus init --env-pattern "MY_CORP_*" --env-pattern "re:^VAULT_"

Combined example

A typical Go project that needs a corporate proxy and a rotating API token:

# Bake the Go toolchain path into the image once (stable, non-secret).
[env]
GOPATH = "/home/node/go"
PATH = "/usr/local/go/bin:$PATH"
GOPROXY = "https://proxy.corp.internal/go,direct"

# Forward secrets and session-specific values from the host at runtime.
env_passthrough = ["ANTHROPIC_API_KEY", "MY_CORP_*", "re:^VAULT_"]

[logging] — log level and file

Control logging from your user-global config.toml (~/.config/agent-circus/config.toml):

[logging]
level = "DEBUG"                      # DEBUG | INFO | WARNING | ERROR | CRITICAL
file  = "/tmp/agent-circus.log"      # optional; omit to log to stdout only

The --log-level and --log-file CLI flags (and their LOGLEVEL / LOGFILE environment variables) take precedence over config.toml. This section is read from the user-global config only — project-local logging config is not supported because logging is initialised before the workspace is known.

Hooks

Hooks let you inject custom shell commands into the Docker image build, without modifying the shared Dockerfile.

ScriptRuns asWhenTypical use
base-root.shrootBuild timeInstall apt packages, system-level configuration
base-user.shnodeBuild timeInstall npm/uv/pip packages, user-level tooling
startup.shnodeContainer startProject-specific runtime setup

Build scripts are optional and removed from the image after execution. startup.sh is read directly from the bind-mounted workspace at container start — changes take effect immediately without a rebuild.

Inline hooks via config.toml (instant mode)

In instant mode you can define hook content directly in config.toml using the [hooks] table. This works without a .agent-circus/ directory and is ideal for user-global hooks that apply to every project:

[hooks]
base_root = """
apt-get update && apt-get install -y ripgrep fd-find
"""

base_user = """
npm install -g @myorg/custom-tool
curl -fsSL https://my-tool.sh | sh
"""

startup = """
uv sync
"""

Values are multi-line TOML strings written verbatim as shell scripts. A #!/usr/bin/env bash shebang is prepended automatically when absent. If both a [hooks] table and project hook files exist, config.toml wins.

Note: startup.sh defined in config.toml is written to the XDG state directory and bind-mounted into the container at /workspace/.agent-circus/hooks/startup.sh. It takes precedence over any startup.sh file in the workspace. base_root and base_user are ignored in deploy mode — place those scripts in .agent-circus/hooks/ directly.

Hook files (deploy mode)

In deploy mode, place scripts under .agent-circus/hooks/:

# .agent-circus/hooks/base-root.sh
apt-get update \
    && apt-get install -y ripgrep
# .agent-circus/hooks/base-user.sh
npm install -g @myorg/custom-tool
# .agent-circus/hooks/startup.sh
# Sync project dependencies and start an SSH tunnel to an internal host.
uv sync
autossh -M 0 -f -N -L 5432:db.internal:5432 tunnel-user@bastion.internal

Setting up Editors to Work with ACP

Emacs

This is a working agent-shell configuration based agent-circus:

(defconst rpo/agent-shell--container-workspace-path "/workspace/"
  "The workspace path inside agent containers.")

(defun rpo/agent-shell--resolve-container-path (path)
  "Resolve PATH between local filesystem and container workspace.

For example:

- /workspace/README.md
    => /home/xenodium/projects/kitchen-sink/README.md
- /home/xenodium/projects/kitchen-sink/README.md
    => /workspace/README.md"
  (let ((cwd (agent-shell-cwd)))
    (if (string-prefix-p cwd path)
        ;; Local -> container
        (string-replace cwd rpo/agent-shell--container-workspace-path path)
      ;; Container -> local
      (if agent-shell-text-file-capabilities
          (if-let* ((_ (string-prefix-p rpo/agent-shell--container-workspace-path path))
                    (local-path (expand-file-name
                                 (string-replace rpo/agent-shell--container-workspace-path cwd path))))
              (or
               (and (file-in-directory-p local-path cwd) local-path)
               (error "Resolves to path outside of working directory: %s" path))
            (error "Unexpected path outside of workspace folder: %s" path))
        (error "Refuse to resolve to local filesystem with text file capabilities disabled: %s" path)))))

(defun rpo/agent-shell-circus-runner-multi (buffer)
  "Return the agent-circus exec command prefix to run for BUFFER's agent.

Looks up the agent identifier in BUFFER's `agent-shell' config and
selects the matching `agent-circus exec` service, defaulting to
\"claude-code\" when no identifier-specific override is found.

Works in both instant mode and deploy mode."
  (let* ((cfg (agent-shell-get-config buffer))
         (id  (map-elt cfg :identifier))
         (service
          (pcase id
            ('claude-code "claude-code")
            ('codex "codex")
            ('mistral-vibe "mistral-vibe")
            ('opencode "opencode")
            (_ "claude-code"))))
    (list "agent-circus" "--log-level" "DEBUG" "--log-file" "/tmp/agent-circus.log" "exec" service "--")))

(use-package agent-shell
  :ensure t
  :config
  (setq agent-shell-mistral-authentication
        (agent-shell-mistral-make-authentication :api-key "ignored"))
  (setq acp-logging-enabled t)
  (setq agent-shell-command-prefix #'rpo/agent-shell-circus-runner-multi)
  (setq agent-shell-path-resolver-function #'rpo/agent-shell--resolve-container-path)
  (setq agent-shell-file-completion-enabled t))

Roadmap

Planned areas of work include:

  • Expanded Docker and Podman compatibility testing across runtime and Compose provider versions, plus continued hardening for additional container engines.
  • A plugin mechanism that keeps core isolation features small and focused while making integrations easier to add, share, and select per project.

Plugin Mechanism

The plugin infrastructure is intended to separate core mechanisms such as filesystem and network isolation from optional components. New agent harnesses, sidecars, and integrations should be addable without changing the core, and add-ons such as llama.cpp or Claude-Mem should become easier to package, share, and enable only where they are needed, e.g. per-project.

Disclosure: AI-Assisted Development

This project is in large parts AI-generated code. However, humans manage the architecture and perform code reviews of those parts which are generated by AI.

Furthermore, this is a self-hosting project. Agents working on this project are running inside what agent-circus provides. To reduce model risk, AI systems from different vendors are assigned distinct roles in the development process (implementation, testing, and security review).

Contributors

rpoisel

220 commits

Languages

Python

96.6%

Shell

1.9%

Dockerfile

1.0%