Claude Code & OpenCode inside isolated containers — safe, persistent, and project-aware.
ai-pod manages per-workspace containers that run Claude Code or OpenCode. It works with Podman (preferred) or Docker — whichever is available on your system. Each workspace gets a dedicated container, a shared background server bridges host interaction via MCP, and your personal agent settings follow you everywhere.
Features
- Workspace isolation — each directory gets its own container, named by a hash of its path; projects can't interfere with each other
- Persistent agent state — a named volume preserves
~/.claudeand~/.config/opencode(login, memory, settings) across container restarts - Credential scanning — scans the workspace for secrets before mounting it; prompts you to review or abort
- Custom Dockerfiles per project — drop an
ai-pod.Dockerfilein any project to install extra runtimes, tools, or MCP servers - AI-driven skill file — container environment context and host-command usage are delivered via an auto-generated ai-pod skill loaded by Claude and OpenCode
- Host command execution via MCP — the in-container agent talks to the shared host server over MCP (
http://host.containers.internal:7822/mcp); every host command requires your explicit approval with a persistent allowlist - File-based command output — every command writes stdout/stderr/exit to
{workspace}/.ai-pod/commands/{session_id}/{command_id}/so the agent reads long-running output directly - Interactive TUIs —
ai-pod commandsto inspect/kill running host commands,ai-pod allowedto manage the whitelist - Desktop notifications — Stop hooks notify you on Claude session end, and an OpenCode plugin sends notifications when
session.idlefires - Transparent host networking — containers reach host services at
host.containers.internal(Podman) orhost.docker.internal(Docker); no manual port mapping needed - Auto-update checks — silently checks for new releases on startup and notifies you when one is available
Requirements
- Podman or Docker (Podman is preferred; Docker is used as a fallback if Podman is not found)
- Rust (to build from source)
Installation
Quick install (Linux & macOS)
curl -fsSL https://raw.githubusercontent.com/mismosmi/ai-pod/main/install.sh | bashDownloads the latest release binary for your OS and architecture and places it in ~/.local/bin/.
Build from source
Usage
ai-pod [OPTIONS] [COMMAND]
Launch the agent in the current directory
Launch in a specific directory
ai-pod --workdir /path/to/project
Options
| Flag | Description |
|---|---|
--workdir <PATH> |
Use a specific workspace directory (default: cwd) |
--rebuild |
Force a rebuild of the container image |
--no-cache |
Build the image without the Docker/Podman layer cache |
--no-credential-check |
Skip scanning the workspace for credential files |
--dry-run |
Print podman/docker commands instead of executing them |
Subcommands
| Command | Description |
|---|---|
init [--workdir PATH] [--agent ...] [--image ...] |
Create an ai-pod.Dockerfile in the workspace |
build |
Build the container image without launching |
attach |
Attach to a running ai-pod container session |
list |
List all ai-pod containers |
clean [--workdir PATH] |
Stop and remove the container for a workspace |
run <command> [args...] |
Run a command in the container instead of the default |
commands [list|run|kill|logs] |
View/manage host commands (interactive TUI if no subcommand) |
services [list|logs|stop] |
View/manage service containers started by agents (interactive TUI if no subcommand) |
allowed [list|add|remove] |
Manage the always-allowed command whitelist (interactive TUI if no subcommand) |
mask <dir> [--workdir PATH] |
Shadow-mount /app/<dir> with an isolated per-workspace volume |
unmask <dir> [--workdir PATH] |
Stop masking <dir> and delete its shadow volume |
serve |
Start the shared MCP server manually (normally auto-started) |
update |
Fetch the latest install script and run it to upgrade |
Run a specific command in the container
ai-pod run claude resume # resume the last Claude session ai-pod run bash # open a bash shell in the container
IDE integration via ACP
ai-pod run forwards stdio transparently between the parent process and the in-container command. When stdin is not a terminal — i.e. an IDE is piping JSON-RPC over ai-pod's stdio — ai-pod drops the pseudo-TTY allocation and keeps status output on stderr, so the byte stream coming out of the container is exactly what the IDE sees. That makes any agent that speaks the Agent Client Protocol usable from inside the container.
Run your workspace through ai-pod once first, so the credential triage and home volume are set up. Then point your IDE at ai-pod run … with the in-container ACP binary as the command. For Claude Code:
For OpenCode, use whichever ACP entry point it exposes (e.g. ai-pod run opencode acp). Anything you install into your ai-pod.Dockerfile is on $PATH inside the container, so npm i -g @zed-industries/claude-code-acp in the Dockerfile is enough to make the example above work.
Notes:
- Pass
--no-credential-check(or runai-podinteractively first to triage the workspace) — the credential dialog can't run without a TTY, and ai-pod will refuse to start if anything is pending. --workdiris required when the IDE launchesai-podfrom a directory other than the workspace root.
Masking host directories
Some directories — node_modules, target, .venv, dist — contain
artifacts the container produces and the host can't (or shouldn't) reuse.
Mask them so the container gets its own per-workspace storage instead of
overlaying the host's:
ai-pod mask node_modules # next launch mounts an isolated volume at /app/node_modules ai-pod unmask node_modules # stop masking and delete the volume
The shadow volume is named ai-pod-<workspace-hash>-mask-<dir> and is
removed automatically by ai-pod clean. Only top-level directory names are
accepted (no slashes, no hidden dirs). Changes apply to the next container
launch; a warning is printed if a container is currently running.
Configuration
Your host ~/.claude/CLAUDE.md and ~/.claude/settings.json are merged with container defaults at launch time, and your ~/.claude.json is copied in on first init, so your personal Claude preferences carry over automatically.
The MCP server entry for ai-pod is written into ~/.claude.json (mcpServers.ai-pod) and injected into OpenCode via the OPENCODE_CONFIG_CONTENT env var, both with the per-session credentials baked in literally — no env-var interpolation, so claude doctor stays clean.
Per-workspace Dockerfiles
Each workspace can have its own ai-pod.Dockerfile that customizes the container image — installing extra runtimes, tools, or MCP servers.
To create one in the current directory:
This writes an ai-pod.Dockerfile to the workspace root based on the default image. Edit it to add anything your project needs (e.g. Node, Python, Playwright, project-specific MCP servers). When ai-pod launches, it automatically uses ai-pod.Dockerfile if present, otherwise falls back to the global default.
The default image is based on Ubuntu. The Dockerfile downloads the agent (Claude Code or OpenCode) via curl http://${HOST_GATEWAY}:7822/install/{agent}.sh — the shared host server vends per-agent install scripts. The generated Dockerfile includes commented-out examples for common additions like Playwright and MCP servers.
Host interaction
The in-container agent talks to the host through an MCP server running on the shared ai-pod host server (http://host.containers.internal:7822/mcp, or host.docker.internal on Docker). No CLI binary is shipped into the container — host interaction happens entirely through MCP tools, taught to the agent via the auto-generated ai-pod skill.
MCP tools
| Tool | What it does |
|---|---|
run_command |
Run a shell command on the host. Waits up to 5 s; returns inline result if finished, otherwise returns a command_id to poll. |
command_status |
Check the status of a previously started command. Returns running/finished/killed plus the last 10 lines of stdout/stderr. |
stop_command |
Stop a running command (SIGTERM, then SIGKILL after 5 s). |
list_commands |
List commands for this session (or workspace-wide with scope=workspace). |
notify_user |
Send a desktop notification to the host user. |
list_allowed_commands |
List host commands previously approved by the user for this workspace. |
start_service |
Start an auxiliary service container (e.g. postgres:16) reachable from inside the agent container. |
stop_service |
Stop and remove a service container started by this session. |
list_services |
List service containers started by this session. |
service_logs |
Read the tail of a service container's logs. |
Service containers
The agent can spin up auxiliary containers (postgres, redis, …) it needs
for the task at hand by calling the start_service MCP tool. Each
request specifies an image, a short name, optional env vars, and
optional command override. The host user approves the image plus the
sorted list of env-var KEY names (values stay private and never
enter the on-disk allowlist); re-requesting the same image with the
same set of keys is auto-approved.
Service containers live on a per-workspace bridge network
(ai-pod-<workspace-hash>-net). The agent reaches a service by the
name it requested, on the service's standard port — e.g. asking for
name=postgres image=postgres:16 makes it reachable from the agent
container as postgres:5432. No host port mapping is created.
Services are ephemeral. A fresh anonymous volume is allocated each
session and discarded when the session ends; the service container
itself is removed as soon as the main ai-pod container exits (or, as a
backstop, by a periodic sweep in the shared server). ai-pod clean
also removes the per-workspace network.
Inspecting services from the host
ai-pod services # interactive TUI ai-pod services list # plain list across all sessions ai-pod services logs <name> [--lines N] # tail logs of a service ai-pod services stop <name> # stop a running service
The --session <id> flag disambiguates when the same name is in use
across concurrent sessions on the same workspace.
Command output files
Every host command writes its stdout, stderr, and exit code to files on disk that the agent can read directly:
{workspace}/.ai-pod/commands/{session_id}/{command_id}/
stdout # full output stream
stderr # full output stream
exit # decimal exit code, or "killed"
command # the shell command string
The workspace is mounted at /app inside the container, so the agent reads these files with its normal Read tool. ai-pod init offers to add .ai-pod to your .gitignore automatically when the workspace is a git repo.
Inspecting host commands from the host (TUI)
ai-pod commands # interactive TUI: list, view tails, kill ai-pod commands list [--all] # plain list (single session, or every session in the workspace) ai-pod commands run <cmd> # run a host command (same approval flow as the agent) ai-pod commands kill <id> # stop a running command ai-pod commands logs <id> # print stdout/stderr/exit for a command
TUI keybinds: ↑/↓ navigate, Tab toggle stdout/stderr, k kill the selected running command, r force refresh, q quit.
Managing the whitelist
ai-pod allowed # interactive TUI: list approved commands, delete with `d` ai-pod allowed list ai-pod allowed add <command> ai-pod allowed remove <command>
When a host command isn't on the allowlist, the agent's request triggers an approval dialog on the host (60 s timeout). Approve once and it's persisted.
Security
Credential scanning
Before mounting your workspace, ai-pod scans for common credential files (.env, SSH keys, API token files, etc.) and prompts you to continue or abort. Pass --no-credential-check to skip this if you know the workspace is clean.
Keeping .env files out of the container
Move your .env file outside the workspace and symlink it back:
mkdir -p ~/.env-files/my-project mv .env ~/.env-files/my-project/.env ln -s ~/.env-files/my-project/.env .env
The symlink target is outside the mount — the container never sees the actual file. Your app still works on the host.
Host command approval
Claude can only run host commands you have explicitly approved via the interactive prompt. Approved commands are persisted per-workspace so you only approve each one once. The MCP server pre-rejects obviously dangerous patterns (e.g. starting with cd /, piping to | head/| tail) before they reach the approval dialog.
Marketing website
A static marketing site lives in website/index.html. Open it in any browser — no build step required.