Each tool runs in its own container image with its own Node.js runtime. The engine is auto-detected/chosen per OS:
| OS | Engine |
|---|---|
| macOS | Apple container (native, Apple Silicon only) |
| Linux | Podman (rootless) — or rootless Docker |
| Windows | WSL Containers (wslc.exe) — native. |
Image stores are per-engine and per-machine — build once on each machine you use (there's no shared cache between Apple container, Podman, and wslc on another box).
macOS / Linux:
./build
Installs wrapper scripts to ~/.local/bin/claude, ~/.local/bin/codex,
~/.local/bin/agy, ~/.local/bin/opencode. Make sure ~/.local/bin is on your PATH.
By default every tool is built. To rebuild only some of them, pass their names:
./build claude agy
Only the images and wrapper scripts for the tools you name are (re)built and installed.
If you only changed the wrapper scripts in bin/, install them without rebuilding the images (tool names work here too):
./build --scripts-only
To prune dangling images left over by previous builds after (re)building (tool names work here too):
./build --prune-images [tools]
On macOS, container requires Apple Silicon and macOS 15+ (full support on macOS 26). If the daemon isn't running yet, build starts it for you; on some setups you may need to run container system start again after a reboot.
Windows:
WSL Containers is currently a preview feature. One-time setup, in an elevated PowerShell:
wsl --update --pre-release
wsl --shutdown
Reopen your terminal, then confirm it's there:
wslc --version # expect 2.9.3 or higher
Then build and install:
./build.ps1
Installs wrapper scripts to %LOCALAPPDATA%\Programs\Bin. Add that to your PATH and claude, codex, agy, opencode work from PowerShell or cmd.exe.
As on macOS/Linux, every tool is built by default; pass names to build only some:
./build.ps1 claude agy
To install only the wrapper scripts, without rebuilding the images:
./build.ps1 -ScriptsOnly
To prune dangling images left over by previous builds after (re)building:
./build.ps1 -PruneImages [tools]
On Windows it uses wslc.exe that ships as part of WSL itself and runs each container in its own lightweight Hyper-V VM.
WSL:
To make Podman network work on WSL, the slirp4netns package must be installed:
sudo apt update && sudo apt install -y slirp4netns
Then /etc/containers/containers.conf the following lines must be added:
[network]
default_rootless_network_cmd = "slirp4netns"
Same on every OS — from any project directory:
cd ~/my-project
claude
codex
copilot
agy
opencode
The current directory is mounted into the container at /workspace/<hash>/<folder>, so each tool only sees the project you're in. The folder keeps its own name inside the container. The hash is taken from the host path, so every project gets its own path inside the container. That matters because the tools keep per-project state (sessions, memory, folder trust) by path. With one shared path, every project would be treated as the same one. Moving or renaming a project on the host gives it a new path, so its earlier sessions won't show up for it anymore.
A Git repo cloned on Windows has CRLF files on disk and LF in the index, which the host's global core.autocrlf=true hides. The container doesn't see your Windows gitconfig, so without help git status would show every file as modified. The Windows wrappers (bin/*.ps1) therefore pass these settings to Git inside the container:
core.autocrlf=true
core.filemode=false
They are set through GIT_CONFIG_* environment variables, so only Windows runs are affected and the images stay portable. Two things to know:
- A committed
.gitattributesstill takes precedence over these settings. - The environment settings override the repo's own
.git/config. If you deliberately keep a repo LF-only on Windows (core.autocrlf=false), agit checkoutorgit stash popinside the container can rewrite files to CRLF.
To check what Git sees inside the container:
git config --show-origin --get-all core.autocrlf # which setting applies, and from where
git ls-files --eol # index (i/) vs disk (w/) line endings
git diff --ignore-cr-at-eol --stat # is anything really changed?
If you still see phantom diffs, you can set the same values in the repo itself (stored in .git/config, so nothing is committed and collaborators are unaffected):
git config core.autocrlf true
git config core.filemode false
It also helps to add this to the project's CLAUDE.md / AGENTS.md: "This working tree uses Windows line endings. Never convert or 'fix' line endings. Verify real changes with git diff --ignore-cr-at-eol." Don't run dos2unix in the container: it only moves the phantom changes to the Windows side and breaks .bat/.cmd files.
The tools run as root inside their containers. That root is not host root: rootless Podman maps it to your own user, rootless Docker does the same, and Apple container and wslc run each container in its own lightweight VM. On Linux the scripts refuse to run against a non-rootless (system) Docker daemon, because there container root would be host root — set up rootless Docker instead:
dockerd-rootless-setuptool.sh install
You can check what you're on with:
podman info --format '{{.Host.Security.Rootless}}' # expect true
docker info --format '{{.SecurityOptions}}' | grep rootless
Claude skip-permissions mode: the Claude image sets IS_SANDBOX=1, so claude --dangerously-skip-permissions works inside the container (Claude Code otherwise refuses it when running as root). It is still opt-in: plain claude asks for permissions as usual.
Podman/Docker hardening (Linux): the wrappers add
--security-opt no-new-privileges
--cap-drop=ALL --cap-add=CHOWN,DAC_OVERRIDE,FOWNER,FSETID,SETUID,SETGID
Processes can't gain privileges through setuid binaries, and the container drops network admin, raw sockets, kill, chroot and capability-editing privileges. The kept capabilities are what file handling and apt-get install inside the container need. Podman and Docker must run rootless — the scripts check and refuse otherwise (see Sandbox above); with sudo podman or a system dockerd, container root is host root.
Each tool's config/credentials persist in its own home directory, so you only log in once per tool:
~/.cli-tools/claude-home (macOS/Linux)
~/.cli-tools/codex-home
~/.cli-tools/copilot-home
~/.cli-tools/antigravity-home
~/.cli-tools/opencode-home
%USERPROFILE%\.cli-tools\claude-home (Windows, same idea)
%USERPROFILE%\.cli-tools\codex-home
%USERPROFILE%\.cli-tools\copilot-home
%USERPROFILE%\.cli-tools\antigravity-home
%USERPROFILE%\.cli-tools\opencode-home
Browser-based OAuth login (Claude, Antigravity) prints a URL/code to paste into your host browser — same experience as logging in over SSH.
OAuth do not work for Antigravity for personal accounts, on this case a .env file can be created configuring the GEMINI_API_KEY obtained from Google AI Studio on antigravity-home directory.
Device-code login should be used on Codex and Copilot because browser flow won't work in this containerized setup. OAuth callback server is hardcoded to bind 127.0.0.1 inside its own process, so the redirect from your host browser can never reach it — no port-forwarding fix is possible. Use device-code login instead.
Device-code login is disabled by default on OpenAI accounts and must be turned on before it will work:
- Personal account: ChatGPT → Settings → Security → enable device code login.
- Team/Enterprise/Edu workspace: a workspace admin must enable it under Workspace Settings → Permissions → "Allow device code login" — it's greyed out for regular members otherwise.
Alternatively, set an API key before running:
export ANTHROPIC_API_KEY=... # macOS/Linux
export OPENAI_API_KEY=...
export GEMINI_API_KEY=...
$env:ANTHROPIC_API_KEY = "..." # Windows PowerShell
OpenCode is provider-agnostic: it picks up whichever of ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY/GOOGLE_API_KEY is set (all are passed through by the wrapper script), or you can run opencode auth login inside the container to store credentials in opencode-home.
On Windows this just works — wslc.exe is a native Windows process, so the scripts read $env:... directly and pass it into the container.
No WSL-side environment configuration needed.