# agent-container Run OpenCode, Claude Code, and Pi (pi.dev) inside a shared Arch Linux Docker container that closely mirrors a local development environment, while limiting access to sensitive files on the host. ## Features - Tools are not baked into the image: OpenCode, Claude Code, and Pi are bootstrapped into the persistent container home on first use (official installers, no sudo), so `pi update`, `claude update`, and `opencode upgrade` work inside the container - Runs as the host user (same username, UID, GID) - **Per-project isolation**: Each project gets its own container (identified by project path hash) - **Shared persistent home**: All containers mount the same home directory from XDG_DATA_HOME, allowing tools and credentials to persist across projects - **Sudo access**: Agents can install project-specific dependencies that persist in the stopped container - **Hard linking support**: Can hard link files like `~/.gitconfig` to share configurations with containers - **Local variants**: `opencode-local`, `claude-local`, and `pi-local` run the tools directly on the host, bypassing the container - Mounts only the current project directory (same absolute path inside container) - **Security boundary**: No access to SSH keys, passwords, or full `$HOME` (intentionally prevents remote code pushes) ## Install On a fresh machine, two steps are all it takes. The agent tools themselves are not part of the image and need no installation step: on first use, agent-container.py automatically runs each tool's official installer inside the container (no sudo), just like it builds the Docker image automatically if it is missing. ```sh cd ~/Projects/ git clone https://git.jeena.net/jeena/agent-container.git ``` Source the helper file `agent.aliases` in your shell configuration (`.bashrc` or `.zshrc`): ```sh source ~/Projects/agent-container/agent.aliases ``` This makes the `opencode`, `claude`, `pi`, and `agent-container` commands available in new sessions, plus the `opencode-local`, `claude-local`, and `pi-local` variants that run the tools directly on the host. That is the whole install. From any project directory, `opencode`, `claude`, and `pi` now work: the first run installs the tool into the persistent container home, where logins (via `/login`), settings, and history persist across projects, containers, and image rebuilds. ### Optional: share agent state with the host By default, host and container keep separate state: the tools live entirely in the container home, and nothing in your real `$HOME` is moved or linked. If you also run the tools on the host (the `*-local` variants) and want shared logins, settings, and history, run the setup script once: ```sh ~/Projects/agent-container/setup.sh ``` It shows a plan first and asks for confirmation before moving anything (`--dry-run` previews without changes, `--yes` skips the question). Existing state is moved into the container home and symlinked back into `$HOME`. See "Sharing agent state between host and container via symlinks" below. The script is safe to re-run and skips items it cannot handle safely. ### Optional extras ```sh # Share your git config with the containers (hard link): ln ~/.gitconfig ~/.local/share/agent-container/container-home/.gitconfig # Desktop notifications when OpenCode needs attention: mkdir -p ~/.local/share/agent-container/container-home/.config/opencode/plugins cp plugins/opencode/osc99-notify.js \ ~/.local/share/agent-container/container-home/.config/opencode/plugins/ ``` The container home directory at `$XDG_DATA_HOME/agent-container/container-home/` serves as a central `$HOME` inside every container, independent of which project directory you start in. Everything written to `$HOME` inside the container persists there. ## Environment Variables Host environment variables matching the following prefixes are automatically forwarded into the container: - `OPENCODE_*` -- passed through to OpenCode - `ANTHROPIC_*`, `CLAUDE_*` -- passed through to Claude Code - `PI_*` plus provider key prefixes (`ANTHROPIC_*`, `OPENAI_*`, `GEMINI_*`, `GROQ_*`, `OLLAMA_*`, and others) -- passed through to Pi. Alternatively log in once with `/login` inside the container; credentials persist in the shared container home. This means inline overrides work as expected: `OPENCODE_CONFIG=foo opencode` For persistent environment variables, add them to `container-home/.bashrc` (or `.bash_profile` / `.profile`). Tools run through `bash -l` inside the container, so standard shell config files are sourced automatically. Default shell config files from `/etc/skel` are seeded into the container home on first run. - `XDG_DATA_HOME`: Override default data directory (default: `~/.local/share`) ## Usage From any project directory: ```sh # Run OpenCode opencode # Run Claude Code claude # Run Pi pi # Run a tool directly on the host, bypassing the container opencode-local ``` The image is built automatically on first use if it does not already exist. The tool starts inside the container with the current directory mounted and set as the working directory. ### Updating The tools are not part of the image: on first use, each tool's official installer runs inside the container (no sudo) and installs into the persistent container home. They update themselves there, and updates survive container recreation and image rebuilds: ```sh agent-container pi update # or run `pi update` inside a session agent-container claude update agent-container opencode upgrade ``` To rebuild the base image with a fresh Arch Linux userland: ```sh agent-container update ``` This rebuilds the image with a freshly pulled base but keeps all existing containers and everything installed in them. The persistent home directory is not affected. ### Purge To remove all containers, the image, and the persistent home directory: ```sh agent-container purge ``` Note that this also deletes the shared agent state that lives in the container home (`.claude`, `.pi`, ...). The symlinks in your `$HOME` will dangle afterwards. ## Sharing host config files via hard links The container home at `~/.local/share/agent-container/container-home/` is mounted as `/home/` inside every container. You can hard link files from your real `$HOME` into this directory so the container sees them without copying or syncing. Because both paths live on the same filesystem, a hard link means they are literally the same file -- changes from either side are instantly reflected. Example for `.gitconfig`: ```sh mkdir -p ~/.local/share/agent-container/container-home ln ~/.gitconfig ~/.local/share/agent-container/container-home/.gitconfig ``` Avoid linking sensitive files such as `~/.ssh/id_*` or `~/.gnupg/` -- keeping those out of the container is an intentional security boundary. ## Sharing agent state between host and container via symlinks The repository ships a `setup.sh` script that automates the following for `.claude`, `.claude.json`, `.pi`, `.agents`, and the OpenCode state directories. The manual recipe for any other directory looks like this: State directories such as `.claude/`, `.pi/`, or `.agents/` can be shared between the containerized tools (`claude`, `pi`, `opencode`) and their local variants (`claude-local`, `pi-local`, `opencode-local`). Keep the real data in the container home and symlink to it from your real `$HOME`: ```sh mkdir -p ~/.local/share/agent-container/container-home mv ~/.claude ~/.local/share/agent-container/container-home/.claude ln -s ~/.local/share/agent-container/container-home/.claude ~/.claude ``` The direction matters: the real directory must live in the container home, and the symlink must be in the host `$HOME`. The container mounts the container home as `/home/`, so inside the container the path is a regular directory. The host `$HOME` is never mounted into the container, so a symlink pointing the other way would not resolve inside the container. On the host, `~/.claude` resolves to the same data the container sees, so logins, settings, and history are shared by both sides without copying or syncing. The same works for single files like `.claude.json` if a tool needs them on the host too. Repeat the two steps for other state directories (`.pi`, `.agents`, `.config/opencode`, ...) as needed. If `XDG_DATA_HOME` is set to a non-default location, use that path instead. As with hard links, never share sensitive files such as `~/.ssh/id_*` or `~/.gnupg/`. ## OpenCode plugins The repository ships an OpenCode plugin in `plugins/opencode/`: - `osc99-notify.js`: sends desktop notifications via the OSC 99 escape sequence when OpenCode needs attention (idle, question, permission prompt, error). Notifications only fire while the terminal is unfocused. Tested with Kitty. To enable it inside the container, copy it into the OpenCode plugin directory of the container home: ```sh mkdir -p ~/.local/share/agent-container/container-home/.config/opencode/plugins cp plugins/opencode/osc99-notify.js \ ~/.local/share/agent-container/container-home/.config/opencode/plugins/ ```