Run AI coding agents — Claude Code and OpenAI Codex — safely inside disposable Docker containers with persistent agent data. Point it at a project, get a live read-write bind mount of your code into the container, and run agents with --dangerously-skip-permissions (Claude) or --dangerously-bypass-approvals-and-sandbox (Codex) without risking your real system. Pairs with anthropic-image-proxy for image-aware local-model workflows.
See ROADMAP.md for upcoming trust levels and isolation features.
Volume safety (absolute rule). containme never deletes the named volumes that hold your agent's credentials, conversation history, memory, or settings (
claude-data,codex-data,user-npm). Usecontainme resetfor surgical resets — neverdocker volume rm.
- Docker Desktop (Windows/Mac) or Docker Engine + Docker Compose v2 (Linux/WSL)
- Node.js 22+ on the host (to compile and run the CLI)
- npm
- Optional: GitHub CLI (
gh) on the host — used to forward your GitHub token into the container - Optional: a SearXNG instance reachable at
http://host.docker.internal:8086for the SearXNG MCP
git clone https://github.com/pedropachecog/containme
cd containme
npm install
npm run build # compile TypeScript → dist/
npm link # register the `containme` command globally
containme build # build the base + agent Docker imagescontainme build recompiles TypeScript and (re)builds the base image plus all agent images. Run it again whenever you pull updates or change Dockerfiles.
If you'd rather skip npm link, run the CLI directly:
node dist/index.js build
node dist/index.js run .# Claude Code on the current directory (bind mount, full network)
containme run .
# Codex on a specific project
containme run --agent codex /path/to/project
# Local-model workflow (Anthropic-compatible proxy on host port 3456)
npx anthropic-image-proxy --target http://10.0.0.5:8000 --verbose # one terminal
containme run-local . # another terminalFirst run prompts for your local API URL and model name, then saves them to ~/.containme/config.yml so future run-local invocations are zero-prompt.
Build and start an agent session. The project path defaults to ..
| Flag | Default | Description |
|---|---|---|
-a, --agent <agent> |
claude |
Agent to use (claude or codex) |
-t, --trust <level> |
bind |
Trust level — bind is the only implemented value today |
--isolated-caches |
false |
Use session-scoped npm/pip/cargo cache volumes (cleaned up on exit) |
--network <mode> |
full |
Network mode (full or none) |
--api-url <url> |
— | Custom API base URL — used by local-model setups |
--model <model> |
— | Model name override (e.g. unsloth/Qwen3-Coder-Next); appended to the agent's default command |
--prompt <prompt> |
— | Initial prompt to send to the agent |
-e, --env <var> |
— | Environment variable to forward (repeatable). KEY=VALUE sets a literal value; KEY alone forwards from the host environment |
-m, --mount <mount> |
— | Extra bind mount in host:container[:opts] form (repeatable). Mount targets /, /etc, /proc, /sys, /run, /dev are rejected |
Project-level defaults from containme.yml are loaded first; CLI flags always win.
Same as run, but resolves --api-url and --model from ~/.containme/config.yml (prompts on first use), and runs through the local-model workflow.
| Flag | Default | Description |
|---|---|---|
-a, --agent <agent> |
claude |
Agent to use (claude or codex) |
-t, --trust <level> |
bind |
Trust level (bind) |
--isolated-caches |
false |
Session-scoped caches |
--network <mode> |
full |
Network mode (full or none) |
--api-url <url> |
saved | Override the saved API URL for this run |
--model <model> |
saved | Override the saved model name for this run |
--prompt <prompt> |
— | Initial prompt to send to the agent |
-e, --env <var> |
— | Environment variable to forward (repeatable) |
-m, --mount <mount> |
— | Extra bind mount (repeatable) |
--reconfigure |
false |
Re-prompt for API URL and model and overwrite the saved config |
Open a shell. If a containme-labelled container is running for the given project, exec into it; otherwise start a fresh, throwaway shell with the same volumes and credentials. No need to start a full agent session first.
| Flag | Description |
|---|---|
-a, --agent <agent> |
Filter by agent (claude or codex) |
-c, --command <cmd> |
Run a single command non-interactively instead of an interactive shell |
--as-agent |
Exec as the agent user (UID 1000) instead of the default root |
If multiple containers match, an interactive picker is shown.
Compile TypeScript then rebuild the base image and agent images.
| Flag | Description |
|---|---|
-a, --agent <agent> |
Build only one agent image (claude or codex); default is all |
Targeted reset that operates inside the persistent agent volume — the volume itself is never destroyed.
| Flag | Default | Description |
|---|---|---|
-a, --agent <agent> |
claude |
Which agent's data to reset (claude or codex) |
| Target | Claude | Codex |
|---|---|---|
auth |
Deletes ~/.claude/.credentials.json |
Deletes ~/.codex/auth.json |
mcp |
Removes mcpServers from ~/.claude/.claude.json |
Not applicable — prints a message and exits cleanly |
cache |
Removes cache/, statsig/, telemetry/, debug/, paste-cache/, downloads/, stats-cache.json |
Removes ~/.codex/log/ |
Drop a containme.yml at your project root to set defaults. CLI flags always override it.
agent: claude # claude | codex
trust: bind # bind
isolated-caches: false # bool
network: full # full | none
api-url: http://host.docker.internal:3456
model: unsloth/Qwen3-Coder-Next
prompt: "Continue where we left off."
env: # list of KEY=VALUE or KEY (forwarded from host)
- SEARXNG_URL=http://host.docker.internal:8086
- GITHUB_TOKEN
mount: # extra bind mounts in host:container[:opts] form
- /home/me/notes:/workspace/notes:roThe CLI reads the file with yaml's parse; missing or unparseable files are silently ignored.
The agent gets a live read-write bind mount of your project at /workspace. Edits are immediate on the host. This is the only trust level shipped today; additional isolation modes (snapshot, git) are tracked in ROADMAP.md.
Agent data lives in named Docker volumes. containme never deletes these automatically.
| Volume | Mount (inside container) | Defined in | Contains |
|---|---|---|---|
claude-data |
/home/agent/.claude |
compose/docker-compose.claude.yml |
Claude credentials, settings, conversation history, memory, MCP config, .claude.json |
codex-data |
/home/agent/.codex |
compose/docker-compose.codex.yml |
Codex auth, config.toml, history, logs |
user-npm |
/home/agent/.npm-global |
compose/docker-compose.claude.yml, compose/docker-compose.codex.yml |
User-installed npm packages — agent CLIs (Claude Code, Codex), MCP servers, containme-install package list |
npm-cache |
/home/agent/.npm |
compose/docker-compose.yml |
npm package cache (shared across sessions; speeds up installs) |
pip-cache |
/home/agent/.cache/pip |
compose/docker-compose.yml |
pip package cache |
cargo-cache |
/home/agent/.cargo/registry |
compose/docker-compose.yml |
Cargo registry cache |
--isolated-caches swaps the three cache volumes for session-scoped names (containme-npm-<sid>, containme-pip-<sid>, containme-cargo-<sid>) and removes them on exit. The persistent agent volumes (claude-data, codex-data, user-npm) are never affected.
- Ubuntu 24.04 base, non-root
agentuser (UID 1000) - Node.js 22 (copied from
node:22-slim, no curl-pipe install) - Python 3.12 +
pip+python3.12-venv, plusuv - npm globals:
pnpm,yarn git, GitHub CLI (gh),ripgrep,fd-find,jq,curl,wget,unzip,zip,openssh-client,build-essential,gcc,g++,make,cmake- Pandoc — universal document converter (Markdown ↔ DOCX, HTML, etc.)
- LibreOffice headless — Writer, Calc, Impress (
-noguivariants) for DOCX→PDF and layout checks - Playwright + Chromium with
--no-sandboxglue, plus a/opt/google/chrome/chromesymlink so Playwright MCP can find Chrome bubblewrap(Codex image only)containme-installhelper for persistent apt installs
@anthropic-ai/claude-code— installed at runtime into/home/agent/.npm-globalso its built-in auto-update keeps working@neuledge/context— installed globally; registered as thecontextMCP viaclaude mcp add context -- context servesearxngMCP viamcp-searxng— points athttp://host.docker.internal:8086. Run a SearXNG instance on the host or override the URL with-e SEARXNG_URL=...playwrightMCP via@playwright/mcp— pre-configured withchromiumSandbox: falseand--no-sandboxfor in-container browsersget-shit-done(GSD) — installed from thepedropachecog/get-shit-doneGitHub fork (cloned, built, andnode bin/install.js --claude --globalon every start). GSD commands in~/.claude/commands/gsd/*.mdare mirrored to~/.claude/skills/gsd:<name>/SKILL.mdso Claude Code's Skill tool sees them.
@openai/codex— installed at runtime into/home/agent/.npm-global@neuledge/context— registered as an MCP server via an idempotent append to~/.codex/config.tomlget-shit-done— installed from the official@opengsd/get-shit-done-redux@latestnpm package (npx @opengsd/get-shit-done-redux@latest --codex --globalon every start).
Asymmetry note: Claude pulls GSD from the
pedropachecog/get-shit-donefork (via git clone), while Codex pulls from the official@opengsd/get-shit-done-redux@latestnpm package. Both are intentional — don't try to "normalize" them.
Containers are ephemeral, so packages installed at runtime disappear on container recreate. Three options:
# inside a containme container
containme-install pandoc imagemagickInstalls immediately via sudo apt-get install -y (sudo is allow-listed for apt/apt-get/chown only) and records each name in /home/agent/.npm-global/.containme-apt-packages. On every future container start, entrypoint.sh reinstalls anything from that list that isn't already present. Package names are validated against [a-zA-Z0-9_.+-] to block shell metacharacters.
containme run -e CONTAINME_APT_PACKAGES="imagemagick texlive-base" .
containme run -e CONTAINME_NPM_PACKAGES="prettier eslint" .
containme run -e CONTAINME_PIP_PACKAGES="black ruff" .entrypoint.sh validates each list with the same allowlist regex ([a-zA-Z0-9_.+ -]) and installs the packages on startup. They're not persisted — the next container start without the env var won't have them.
Always install MCPs as the agent user so they land on the persistent user-npm volume:
containme bash --as-agent
npm install -g <mcp-package>
claude mcp add <name> -- <command>If you npm install -g as root, the package goes to /root/.npm-global (or wherever root's prefix points) inside an ephemeral filesystem and disappears on the next container recreate.
@neuledge/context, searxng, and playwright are pre-registered for Claude on first start. For Codex, only context is wired up by default.
Local LLM servers (llama.cpp, vLLM, etc.) speak OpenAI's Chat Completions format, but Claude Code calls Anthropic's API and reads images from tool_result content blocks. Local servers either reject those images or silently drop them — so Claude Code can't see screenshots when pointed straight at, e.g., llama-server.
anthropic-image-proxy sits between Claude Code and the local model and rewrites image-bearing tool results into user-message content the local model can actually see. containme run-local defaults --api-url to http://host.docker.internal:3456, which is where the proxy listens by default.
# 1. Start the proxy on the host, pointing at your local model server
npx anthropic-image-proxy --target http://10.0.0.5:8000 --verbose
# 2. Start the agent — first run prompts and saves api-url + model to ~/.containme/config.yml
containme run-local .
# 3. Re-prompt later if the model or proxy URL changes
containme run-local --reconfigure .Config lives at ~/.containme/config.yml under a local: key with api-url and model.
containme is a thin TypeScript CLI that stacks Docker Compose files rather than mutating a single one. Every containme run invocation:
containme run .
│
├── resolves project path (Windows D:\foo → /run/desktop/mnt/host/d/foo on Docker Desktop)
├── resolves credentials (env → git config → gh auth token → host ~/.claude.json)
├── writes secrets env file to $TMPDIR/containme-secrets-<sid>.env (mode 0600)
├── generates .containme/docker-compose.override.<sid>.yml (NO secrets in this file)
├── builds containme-base (Ubuntu 24.04 + Node + dev tools + Playwright)
├── builds the agent image (FROM containme-base + agent CLI + envs)
└── docker compose run --rm --service-ports agent
-f compose/docker-compose.yml (volumes, caps, host.docker.internal, limits)
-f compose/docker-compose.<agent>.yml (claude or codex overlay)
-f compose/docker-compose.<trust>.yml (currently only bind)
-f .containme/docker-compose.override.<sid>.yml (env, mounts, env_file, labels)
Compose files are read in order; later files override earlier ones. The override file is where everything dynamic lives: container labels (containme.session/agent/trust/project), env vars, extra mounts, the env_file reference, optional model flag, and (with --isolated-caches) per-session cache volume names. Cleanup on exit removes the override YAML, the secrets env file, and any session-scoped cache volumes.
Session IDs are 128-bit random hex (crypto.randomBytes(16).toString("hex")).
Secrets (API keys, GitHub token) are written exclusively to $TMPDIR/containme-secrets-<sid>.env with mode 0600 and referenced via Compose env_file:. They never appear in the override YAML, never in the project directory, and never on the docker argv.
- Agent runs as non-root user
agent(UID 1000) - Linux capabilities dropped to
ALL, then re-added:CHOWN,SETUID,SETGID,DAC_OVERRIDEonly sudoallow-list isapt,apt-get,chown,/bin/chown(no shell, noapt-key, etc.)- Resource limits:
mem_limit: 8g,cpus: 4,pids_limit: 4096,shm_size: 2gb - Forbidden mount targets (rejected before spawn):
/,/etc,/proc,/sys,/run,/dev— also blocks any subpath under those prefixes - Package-name allowlist regex (
[a-zA-Z0-9_.+ -]for env-var lists,[a-zA-Z0-9_.+-]per-package incontainme-install) — rejects shell metacharacters before anyapt-get install - API keys live in a
0600env file in$TMPDIRand are referenced viaenv_file:; never in argv, never in the override YAML, never in the project directory - 128-bit random session IDs (
randomBytes(16)) git config --global --add safe.directory '*'is set inside the container only — needed because bind-mounted host paths trip Git's "dubious ownership" check- Network mode
noneadds--no-depstodocker compose run
src/
index.ts CLI entry — Commander.js, loads containme.yml, dispatches
commands/
run.ts containme run — validation, build, compose-run, cleanup
run-local.ts containme run-local — prompts + saves global model config
reset.ts containme reset auth|mcp|cache
build.ts containme build — tsc + base + agent images
bash.ts containme bash — list/attach or fresh shell
core/
compose-generator.ts RunOptions → override YAML + secrets env file
credential-resolver.ts Resolves API key + git identity + GitHub token + host .claude.json
path-resolver.ts Windows D:\foo → /run/desktop/mnt/host/d/foo translation
global-config.ts ~/.containme/config.yml read/write
agents/
claude-code.ts Claude AgentConfig (defines the AgentConfig interface)
codex.ts Codex AgentConfig
utils/
platform.ts isWindows / isWSL / getShell
package-root.ts walk up from import.meta.url to find package.json
docker/
base.Dockerfile Ubuntu 24.04 + Node 22 + Python 3.12 + Playwright + Pandoc + LibreOffice + dev tools
claude.Dockerfile FROM containme-base + CLAUDE_CONFIG_DIR + CONTAINME_AGENT=claude
codex.Dockerfile FROM containme-base + bubblewrap + CODEX_HOME + CONTAINME_AGENT=codex
scripts/
entrypoint.sh MCP registration, agent CLI install, GSD install, secrets, package restore
containme-install Persistent apt install with name validation
compose/
docker-compose.yml Base service (volumes, caps, limits, host.docker.internal)
docker-compose.claude.yml Claude overlay (claude-data + user-npm volumes, ANTHROPIC_API_KEY)
docker-compose.codex.yml Codex overlay (codex-data + user-npm volumes, OPENAI_API_KEY)
docker-compose.bind.yml Bind trust overlay (mounts ${CONTAINME_PROJECT_PATH} → /workspace)
containme.yml (Optional) per-project defaults
npm install
npm run build # compile TypeScript only → dist/
npm run dev # tsc --watch
npm test # run vitest test suite once
npm run test:watch # vitest watch mode
containme build # full rebuild: tsc + base + agent imagesTests live in test/unit/*.test.ts and test/integration/*.test.ts and run via Vitest.
MIT — see package.json.