contai is a very opinionated Docker-based sandbox for running AI CLI tools
for people paranoid enough to not wanting to give them access to their whole
system (i.e. normal people).
- Sandboxed Environment: Runs AI CLI tools in an isolated Docker container to protect your host system
- Pre-installed AI CLI Tools: Includes OpenCode, OpenAI Codex, GitHub Copilot, Google Gemini, Claude Code, and RTK out of the box
- Automatic RTK Setup: Configures RTK at container startup for the shipped
home-scoped tools, and when launching Copilot also installs its project-scoped
.githubintegration in the current project - User Permission Mapping: Maintains your host UID/GID for seamless file access and ownership
- Persistent Home Directory: Stores configuration and data in
~/.local/share/contai/homeacross container sessions - Current Directory Mounting: Automatically mounts and uses your current working directory
- Development Tools Included: Comes with ripgrep, bat, git, Python, Node.js, and other essential tools
- On-demand Tool Installation: Includes pkgx for installing additional tools without root access
- Symlink-friendly: Can be symlinked as different tool names (e.g.,
opencodesymlink runs OpenCode directly)
On the host you need Docker, usable
by your own user, and bash for the contai script itself. Any version of
bash will do, including the 3.2 that macOS still ships as /bin/bash. The
other scripts are POSIX sh. Everything else lives inside the container.
To build the container image:
./build.shThis will create a Docker image tagged as contai:latest with your host user's
UID/GID for proper file permissions.
To override the account created in the image, set CONTAI_UID, CONTAI_USER,
CONTAI_GID, CONTAI_GROUP, and/or CONTAI_HOME when building:
CONTAI_UID=1000 CONTAI_USER=developer CONTAI_GID=1000 CONTAI_GROUP=developers \
./build.shCONTAI_HOME is the home directory of that account, and defaults to your own
$HOME. contai mounts the persistent home at whatever $HOME is when it
runs, so overriding this at build time and not matching it at run time leaves
the container with a $HOME that nothing is mounted at, and anything written
there is lost when the container exits.
After building, you can install contai to your PATH:
# Create a bin directory in your home (if it doesn't exist)
mkdir -p ~/bin
# Copy the contai script
cp contai ~/bin/
# Make sure ~/bin is in your PATH (add to ~/.bashrc or ~/.zshrc if needed)
export PATH="$HOME/bin:$PATH"You can create symlinks to run specific AI tools directly:
cd ~/bin
ln -s contai opencode
ln -s contai copilot
ln -s contai codex
ln -s contai gemini
ln -s contai claude
ln -s contai rtkNow you can run tools directly (e.g., opencode instead of contai opencode,
or rtk instead of contai rtk).
To enable AI agents to work best inside the container (e.g., using pkgx for
missing tools instead of apt-get), install the provided agent-instructions.md
file to the appropriate location for your AI tool:
# Create the container's home config directories
home=~/.local/share/contai/home
mkdir -p "$home"# OpenCode
mkdir -p "$home/.config/opencode"
cp agent-instructions.md "$home/.config/opencode/AGENTS.md"mkdir -p "$home/.claude"
cp agent-instructions.md "$home/.claude/CLAUDE.md"mkdir -p "$home/.gemini"
cp agent-instructions.md "$home/.gemini/GEMINI.md"mkdir -p "$home/.codex"
cp agent-instructions.md "$home/.codex/AGENTS.md"GitHub Copilot CLI only supports project-level instructions (not global), so
you would need to copy the file to each project's
.github/copilot-instructions.md.
Run AI tools in the sandboxed environment:
# Run a specific tool
contai opencode
# Or use symlinks for direct access
opencode
# The container automatically mounts your current directory
cd /path/to/your/project
contai opencodeYour configuration and data will be persisted in ~/.local/share/contai/home
across container sessions. The directory is mounted at your host home
directory's own path, so $HOME and everything below it is spelled the same
way inside and outside the container, and configuration that records absolute
paths stays valid. The contents are still the container's own: ~ inside the
container is the persistent directory above, not your host home directory.
contai initializes RTK at container startup, not at image build time. This is
necessary because the generated RTK config lives in the mounted container home
directory (~/.local/share/contai/home) and, for Copilot, in the current
project.
On startup, contai automatically configures RTK for:
- Claude Code
- Google Gemini CLI
- OpenAI Codex CLI
- OpenCode
When you launch copilot, contai also installs RTK's project-scoped Copilot
integration in the current project root by creating:
.github/copilot-instructions.md.github/hooks/rtk-rewrite.json
To check how many tokens RTK has saved across your sessions:
contai rtk gainAutomatic RTK setup always disables telemetry during initialization so a first-run consent prompt cannot block container startup. To enable telemetry, configure RTK explicitly from the container:
contai rtk telemetry enableThis prompts for consent and stores the choice in the persistent container home. The automatic startup initialization remains non-interactive.
You can define environment variables in the container by writing to a
~/.local/share/contai/env.list file. The file is expected to have the
standard docker --env-file
format.
Arbitrary docker run options can be added in
~/.local/share/contai/docker-run-opts.list. Each line is one argument,
taken verbatim, so nothing needs quoting or escaping and paths may contain
spaces. Blank lines and lines starting with # are ignored.
Use the --option=value form so an option and its value fit on one line:
--volume=/data/models:/data/models:ro
--network=host
--gpus=all
An option that exists only in the two-word form takes two lines:
--add-host
myhost:10.0.0.1
These are passed after contai's own options, so they win where docker lets a
later option override an earlier one. That includes the options that make
this a sandbox at all: --cap-drop=ALL, --security-opt=no-new-privileges,
--user, and which directories are mounted. Putting --privileged or
--user=root in here gives away most of the point of contai, and so does
mounting this file, or any directory holding it, into the container: whatever
runs there could then choose the options of every later run.
Short of mounting it yourself, nothing running in the container can reach this
file, as only the current directory and the container home are mounted. The
remaining way to expose it is starting contai from a directory that contains
it, ~/.local for instance, since the current directory is mounted writable:
whatever runs in the container could then pick the options of the next run,
and escape. contai refuses to start from such a directory rather than allowing
it.
That check exists to catch the easy mistake, not to be a boundary. It follows symbolic links to find where the file really lives, but it cannot account for every way of making it writable from the container, and does not try to.
Two things are worth keeping in mind when adding --volume.
Create the host directory first. Docker creates a missing bind mount source
itself, but owned by root, which the container user then cannot write to.
Mounting a directory at the same path on both sides keeps absolute paths
recorded inside it valid on both sides. Python virtualenvs are a good
example: they name their interpreter by absolute path in pyvenv.cfg and
bin/python, so a virtualenv is only usable from both the host and the
container when its interpreter is reachable at a single path that exists in
both.
Prefer :ro for anything the host executes. A writable mount lets whatever
runs in the container replace a binary, a cached package or a sourced shell
snippet that the host later runs, which defeats the point of the sandbox.
-
Tested almost exclusively with OpenCode for now.
-
When configuring MCP servers that need OAuth, for example using
mcp-remote, for completing the OAuth flow, you need to open a browser on your host machine. For now, you need to run the OAuth flow outside the container, as the container does not have access to your host's browser, and then copy the credentials manually.For example:
cp -r ~/.mcp-auth/* ~/.local/share/contai/home/.mcp-auth/
-
Add installer script and/or Debian package for easier installation (including symlinks to the shipped tools)
-
Add pre-built images (needs some thought about how to deal with user IDs)
-
Add auto-image build when invoking
contaiif image is not found -
Find a way to complete OAuth flows from within the container
-
Integrate with
docker-composefor easier multi-container setups, for cases where other services are needed, like MCP servers -
Support forwarding environment variables from the current environment.
-
Add configuration file to be able to customize, for example:
- Mapping of directories to mount into the container
- Extra packages to install to the image