Run in a container
When runtime is set to docker or podman in your zooid.yaml, Zooid spawns each agent inside its own container instead of as a child process on the host. This guide covers how the container model works, what state carries over from the host, and the one gotcha you will hit if you run Claude Code on macOS.
What you get for free
Under a container runtime, Zooid wires three layers of bind mounts into every agent container without you having to declare them:
- Workspace — your agent’s
workdiris bind-mounted to/workspaceinside the container. Edits the agent makes are visible on the host, and vice versa. - Preset state — each ACP preset declares a host directory to bind-mount so the agent’s auth, settings, and history persist across container restarts. The host source is the daemon user’s
$HOME, so anything you do on the host with the agent CLI (e.g.codex login) is automatically visible inside the container. - User mounts — anything you declare under
agents.<name>.container.mountsinzooid.yaml. See thezooid.yamlguide for the schema.
The preset state mounts per agent harness:
| Preset | Host | Container |
|---|---|---|
claude | ~/.claude | /root/.claude |
codex | ~/.codex | /root/.codex |
opencode | ~/.local/share/opencode + ~/.config/opencode | matching /root/... paths |
All preset mounts are read-write. The host path must already exist — Zooid will not create it for you. This is intentional: an empty host directory mounted into the container masks “host isn’t logged in” as “agent can’t authenticate”, which is much harder to diagnose. If you see a bind-mount error in the daemon log when you start your first prompt, run the relevant <tool> login on the host first.
Authentication that carries over
codex and opencode store their tokens as plain files inside the directories listed above (~/.codex/auth.json, ~/.local/share/opencode/). Running codex login or opencode auth login on the host is enough — the next time you start zooid dev, the agent inside the container picks up the same credentials with no extra configuration.
Gotcha: Claude Code on macOS uses Keychain, not a file
Claude Code is the one preset where host login does not carry over on macOS. On macOS, claude login writes its OAuth token into the system Keychain (via the security framework), not into ~/.claude/. The home mount carries over your config, your session history, and your .claude/settings.json, but not the credentials — because they aren’t in that directory to begin with.
On Linux hosts this isn’t an issue: Claude Code falls back to ~/.claude/.credentials.json and the home mount works as you’d expect.
Two ways to get a working Claude Code agent in a container on macOS:
Option 1 — ANTHROPIC_API_KEY (simplest)
Skip OAuth entirely and pass an API key via the agent’s container env:
agents: reviewer: acp: { preset: claude } container: env: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}Then set ANTHROPIC_API_KEY in your .env (or shell) on the host. Zooid expands ${...} references against the daemon’s process.env before passing them through.
This works on macOS, Linux, and CI — no Keychain involvement at all.
Option 2 — claude login inside the container
If you want OAuth (e.g. you’re on a Claude subscription, not the API), run claude login once inside the running container. It writes /root/.claude/.credentials.json, which the home mount persists back to ~/.claude/.credentials.json on the host.
# In a separate terminal while `zooid dev` is running:podman exec -it <agent-container> claude loginFind the container name with podman ps (or docker ps). After this completes once, every subsequent agent spawn picks up the credentials file from the home mount.
This bypasses the host Keychain entirely — you now have a Linux-style file-based credential on the host that any future container spawn can read. The downside is that the credentials file is plaintext on disk; treat its directory permissions accordingly.
BYO container image
If you set agents.<name>.container.image to something other than the preset default, you take on the contract:
- The ACP shim binary (
claude-agent-acp,codex-acp, etc.) must be onPATH. - The agent’s
$HOMEinside the container should be/root(or update the preset mount targets to match).
The default Zooid agent images (ghcr.io/zooid-ai/agent-claude-code:latest, agent-codex:latest, agent-opencode:latest) already satisfy both. See the Dockerfiles under zooid/packages/runtime-docker/docker/ for the build recipe if you want to fork.
Skipping the image prepull
zooid dev pulls each agent’s container image at startup so the first prompt isn’t blocked on a docker pull. To skip:
ZOOID_NO_PREPULL=1 zooid dev # skip the inspect-and-pull passZOOID_REFRESH_IMAGES=1 zooid dev # force a re-pull of every imageBoth flags are also exposed as noPrepull and refreshImages on the programmatic startDaemon options.