Run AI coding agents in sandboxed containers communicate via ACP
32
stars
225
commits
Python
primary language
Sep 14, 2026
updated
Run AI coding agents in isolated Docker or Podman containers with one config language.
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).
Agent Circus is built around open interfaces instead of editor- or vendor-specific integrations:
agent-circus ToolThe 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 .
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.
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.
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
There are two modes of operation:
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
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.
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.
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.
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.
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 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.
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:
| Agent | Seed source | Container 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.
Agent Circus can be configured via TOML files. Settings are resolved in this order (last wins):
$XDG_CONFIG_HOME/agent-circus/config.toml
(default: ~/.config/agent-circus/config.toml).agent-circus/config.toml in the workspaceCLI flags override both.
[!IMPORTANT] A
key = valueline 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_passthroughPlace standalone keys before the first
[table]header instead.
initRather 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.
| Flag | Description |
|---|---|
--ssh | Add [ssh] table to enable SSH agent forwarding |
--ssh-config PATH | Set ssh.config_path (implies --ssh) |
--ssh-known-hosts PATH | Set ssh.known_hosts_path (implies --ssh) |
--shadow TEXT | Append a path to the shadow list (repeatable) |
--git | Add [git] table to enable Git config forwarding |
--git-config PATH | Set git.config_path (implies --git) |
--git-signing-key PATH | Set git.signing_key_path (implies --git) |
--hosts-pattern TEXT | Append a pattern to hosts.patterns (repeatable) |
--ca-cert-pattern TEXT | Append a pattern to ca_certs.patterns (repeatable) |
--env-pattern TEXT | Append a pattern to env_passthrough (repeatable) |
--git-worktree-mirror | Set git.worktree_mirror = true and write AGENTS.md |
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:
agent-circus up --runtime podman
agent-circus build --runtime docker
AGENT_CIRCUS_RUNTIME=podman agent-circus up
[runtime]
engine = "podman"
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.
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 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.
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:
| Field | Required | Default | Description |
|---|---|---|---|
model | no | ggml-org/gemma-3-1b-it-GGUF/gemma-3-1b-it-Q4_K_M.gguf | Model passed to llama-server -m. Accepts a HuggingFace path (owner/repo/file.gguf) or a local path inside the container. |
models_cache | no | ${HOME}/.cache/huggingface | Host directory bind-mounted into the sidecar so downloaded models survive restarts. Use an absolute path or Compose environment interpolation (${HOME}/…); ~ is not supported. |
context_size | no | 2048 | Context window size passed to --ctx-size. |
extra_args | no | [] | List of additional raw flags appended to the llama-server command. |
When [llama_cpp] is present, Agent Circus:
llama-cpp sidecar using ghcr.io/ggml-org/llama.cpp:server.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.depends_on: llama-cpp to the opencode service so the sidecar
starts first.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:serverimage 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 theopencodeservice. Other agents are unaffected.
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:
/home/node/.claude-mem.CLAUDE_MEM_DATA_DIR=/home/node/.claude-mem for the claude-code
service.claude-code container starts.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:
| Field | Required | Default | Description |
|---|---|---|---|
enabled | no | false | Enable Claude-Mem for supported services |
scope | no | workspace | Memory scope; currently only workspace is supported |
services | no | ["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-codeservice. 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-codeimage. Runagent-circus build claude-codeafter enabling[claude_mem]for the first time or after updating Agent Circus.
Host config isolation:
agent-circus exec claude-code --host-configis rejected while[claude_mem].enabled = true, because Claude-Mem setup writes Claude Code plugin files and should only modify the workspace-local config store.
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:
| Field | Required | Default | Description |
|---|---|---|---|
path | yes | — | Absolute path on the host |
readonly | no | false | Mount read-only when true |
name | no | basename of path | Container mount name (/workspaces/<name>) |
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:
| Field | Required | Default | Description |
|---|---|---|---|
name | yes | — | Store name; used as the host subdirectory |
mount_path | no | /home/node/.local/share/agent-circus/<name> | Container mount path |
services | no | all agent services | Services that receive the store |
seed_from | no | unset | Host directory copied into the store before first container start; use an absolute path or environment interpolation like ${HOME}/.codex |
seed_mode | no | once | Seed 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.
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:
| Field | Required | Default | Description |
|---|---|---|---|
service | yes | - | Agent service to expose |
container_port | yes | - | Port inside the container |
host_port | no | container_port | Port on the host |
host | no | 127.0.0.1 | Host bind address |
protocol | no | tcp | tcp 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.
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_SOCKmust be set in your environment when runningagent-circus. If it is not, agent-circus will report an error.
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.
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
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.shwhich is baked into the image. Runagent-circus buildafter enabling these options for the first time.
IdentityFile directives: If your
~/.ssh/configreferences 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.
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 buildafter 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 useconfig_pathto point at a dedicated container-friendly config.
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.mdat init time. Re-runagent-circus init --git-worktree-mirrorif the repository is moved to a different location on the host.
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.shand its sudoers rule are baked into the image. Runagent-circus buildafter enabling[ca_certs]for the first time.
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:
| Pattern | Syntax | Example |
|---|---|---|
*.corp.internal | fnmatch glob (default) | matches foo.corp.internal |
myserver | exact glob | matches only myserver |
re:\.vpn\. | Python regex (prefix re:) | matches any name containing .vpn. |
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.
Two complementary mechanisms control environment variables in containers:
| Mechanism | When | Use for |
|---|---|---|
[env] table | Build time — baked into the image as ENV instructions | Stable toolchain paths, non-secret config |
env_passthrough list | Runtime — forwarded from the host when the container starts | Secrets, tokens, values that change per session |
[env] — bake into the imageEach 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 runtimeVariable 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_"
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 fileControl 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 let you inject custom shell commands into the Docker image build,
without modifying the shared Dockerfile.
| Script | Runs as | When | Typical use |
|---|---|---|---|
base-root.sh | root | Build time | Install apt packages, system-level configuration |
base-user.sh | node | Build time | Install npm/uv/pip packages, user-level tooling |
startup.sh | node | Container start | Project-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.
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.shdefined inconfig.tomlis written to the XDG state directory and bind-mounted into the container at/workspace/.agent-circus/hooks/startup.sh. It takes precedence over anystartup.shfile in the workspace.base_rootandbase_userare ignored in deploy mode — place those scripts in.agent-circus/hooks/directly.
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
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))
Planned areas of work include:
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.
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).
220 commits
5 commits
Python
96.6%
Shell
1.9%
Dockerfile
1.0%
Run AI coding agents in sandboxed containers communicate via ACP
32
stars
225
commits
Python
primary language
Sep 14, 2026
updated
Run AI coding agents in isolated Docker or Podman containers with one config language.
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).
Agent Circus is built around open interfaces instead of editor- or vendor-specific integrations:
agent-circus ToolThe 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 .
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.
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.
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
There are two modes of operation:
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
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.
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.
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.
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.
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 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.
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:
| Agent | Seed source | Container 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.
Agent Circus can be configured via TOML files. Settings are resolved in this order (last wins):
$XDG_CONFIG_HOME/agent-circus/config.toml
(default: ~/.config/agent-circus/config.toml).agent-circus/config.toml in the workspaceCLI flags override both.
[!IMPORTANT] A
key = valueline 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_passthroughPlace standalone keys before the first
[table]header instead.
initRather 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.
| Flag | Description |
|---|---|
--ssh | Add [ssh] table to enable SSH agent forwarding |
--ssh-config PATH | Set ssh.config_path (implies --ssh) |
--ssh-known-hosts PATH | Set ssh.known_hosts_path (implies --ssh) |
--shadow TEXT | Append a path to the shadow list (repeatable) |
--git | Add [git] table to enable Git config forwarding |
--git-config PATH | Set git.config_path (implies --git) |
--git-signing-key PATH | Set git.signing_key_path (implies --git) |
--hosts-pattern TEXT | Append a pattern to hosts.patterns (repeatable) |
--ca-cert-pattern TEXT | Append a pattern to ca_certs.patterns (repeatable) |
--env-pattern TEXT | Append a pattern to env_passthrough (repeatable) |
--git-worktree-mirror | Set git.worktree_mirror = true and write AGENTS.md |
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:
agent-circus up --runtime podman
agent-circus build --runtime docker
AGENT_CIRCUS_RUNTIME=podman agent-circus up
[runtime]
engine = "podman"
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.
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 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.
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:
| Field | Required | Default | Description |
|---|---|---|---|
model | no | ggml-org/gemma-3-1b-it-GGUF/gemma-3-1b-it-Q4_K_M.gguf | Model passed to llama-server -m. Accepts a HuggingFace path (owner/repo/file.gguf) or a local path inside the container. |
models_cache | no | ${HOME}/.cache/huggingface | Host directory bind-mounted into the sidecar so downloaded models survive restarts. Use an absolute path or Compose environment interpolation (${HOME}/…); ~ is not supported. |
context_size | no | 2048 | Context window size passed to --ctx-size. |
extra_args | no | [] | List of additional raw flags appended to the llama-server command. |
When [llama_cpp] is present, Agent Circus:
llama-cpp sidecar using ghcr.io/ggml-org/llama.cpp:server.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.depends_on: llama-cpp to the opencode service so the sidecar
starts first.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:serverimage 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 theopencodeservice. Other agents are unaffected.
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:
/home/node/.claude-mem.CLAUDE_MEM_DATA_DIR=/home/node/.claude-mem for the claude-code
service.claude-code container starts.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:
| Field | Required | Default | Description |
|---|---|---|---|
enabled | no | false | Enable Claude-Mem for supported services |
scope | no | workspace | Memory scope; currently only workspace is supported |
services | no | ["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-codeservice. 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-codeimage. Runagent-circus build claude-codeafter enabling[claude_mem]for the first time or after updating Agent Circus.
Host config isolation:
agent-circus exec claude-code --host-configis rejected while[claude_mem].enabled = true, because Claude-Mem setup writes Claude Code plugin files and should only modify the workspace-local config store.
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:
| Field | Required | Default | Description |
|---|---|---|---|
path | yes | — | Absolute path on the host |
readonly | no | false | Mount read-only when true |
name | no | basename of path | Container mount name (/workspaces/<name>) |
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:
| Field | Required | Default | Description |
|---|---|---|---|
name | yes | — | Store name; used as the host subdirectory |
mount_path | no | /home/node/.local/share/agent-circus/<name> | Container mount path |
services | no | all agent services | Services that receive the store |
seed_from | no | unset | Host directory copied into the store before first container start; use an absolute path or environment interpolation like ${HOME}/.codex |
seed_mode | no | once | Seed 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.
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:
| Field | Required | Default | Description |
|---|---|---|---|
service | yes | - | Agent service to expose |
container_port | yes | - | Port inside the container |
host_port | no | container_port | Port on the host |
host | no | 127.0.0.1 | Host bind address |
protocol | no | tcp | tcp 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.
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_SOCKmust be set in your environment when runningagent-circus. If it is not, agent-circus will report an error.
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.
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
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.shwhich is baked into the image. Runagent-circus buildafter enabling these options for the first time.
IdentityFile directives: If your
~/.ssh/configreferences 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.
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 buildafter 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 useconfig_pathto point at a dedicated container-friendly config.
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.mdat init time. Re-runagent-circus init --git-worktree-mirrorif the repository is moved to a different location on the host.
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.shand its sudoers rule are baked into the image. Runagent-circus buildafter enabling[ca_certs]for the first time.
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:
| Pattern | Syntax | Example |
|---|---|---|
*.corp.internal | fnmatch glob (default) | matches foo.corp.internal |
myserver | exact glob | matches only myserver |
re:\.vpn\. | Python regex (prefix re:) | matches any name containing .vpn. |
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.
Two complementary mechanisms control environment variables in containers:
| Mechanism | When | Use for |
|---|---|---|
[env] table | Build time — baked into the image as ENV instructions | Stable toolchain paths, non-secret config |
env_passthrough list | Runtime — forwarded from the host when the container starts | Secrets, tokens, values that change per session |
[env] — bake into the imageEach 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 runtimeVariable 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_"
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 fileControl 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 let you inject custom shell commands into the Docker image build,
without modifying the shared Dockerfile.
| Script | Runs as | When | Typical use |
|---|---|---|---|
base-root.sh | root | Build time | Install apt packages, system-level configuration |
base-user.sh | node | Build time | Install npm/uv/pip packages, user-level tooling |
startup.sh | node | Container start | Project-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.
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.shdefined inconfig.tomlis written to the XDG state directory and bind-mounted into the container at/workspace/.agent-circus/hooks/startup.sh. It takes precedence over anystartup.shfile in the workspace.base_rootandbase_userare ignored in deploy mode — place those scripts in.agent-circus/hooks/directly.
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
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))
Planned areas of work include:
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.
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).
220 commits
5 commits
Python
96.6%
Shell
1.9%
Dockerfile
1.0%