Rootless Podman falls back to a single-UID user namespace when the
invoking user has no /etc/subuid and /etc/subgid entry, and then any
chown to an unmapped UID fails with EINVAL. Newer pacman versions hit
this during builds and updates: their download sandbox chowns its
temporary download directory to the 'alpm' user, which made image
builds fail with a cryptic traceback after several minutes of output.
Accounts created manually or before shadow 4.11.1-3 (October 2022) get
no subordinate ID ranges automatically, while freshly created ones do.
Rather than working around this in the image (disabling pacman's
sandbox would silently drop a security feature), detect the missing
ranges before building and stop with the exact fix, using the first
unclaimed 65536-ID block so the printed command never collides with
other users' ranges. Link the Arch Wiki section and Podman's rootless
tutorial for background.
Agents and skills gate on AGENT_CONTAINER=1 to know they are inside the
container rather than on the host, but nothing told a reader that the
variable exists, or that containers predating it get recreated.
The release notes only help someone who finds them, so the upgrade
section now points at CHANGELOG.md.
Access to the Docker socket is equivalent to host root: whoever can talk
to /var/run/docker.sock can start a privileged container with the host
filesystem mounted and edit any file on it. Membership of the docker
group defeats the containment this tool exists to provide, because an
agent escapes the project directory by asking the daemon, not by
exploiting a bug. Rootless Podman keeps the workflow and drops the
escape: containers run in a user namespace owned by the invoking user,
and container root maps to a subuid from /etc/subuid instead of to host
root.
Rootless containers are created with --userns=keep-id so that the host
UID/GID map to the same values inside the container and bind-mounted
files keep their ownership in both directions; without it the container
UID 1000 would own nothing in the project. Sudo inside the container
keeps working, but only reaches container root.
Docker stays available through AGENT_CONTAINER_RUNTIME=docker for hosts
that cannot run rootless Podman. Existing Docker containers and images
are deliberately left alone: they are not reused, and the first Podman
run builds its own image.
Baked installs had two failure modes: anything installed into $HOME
at build time is shadowed by the container-home bind mount at runtime,
and system-wide installs are root-owned, so the tools' own update
commands (pi update, claude update, opencode upgrade) failed with an
unwritable install path. With pi releasing new versions almost daily,
the only remedy, a full image rebuild that also deleted every
container, was too heavy for that cadence.
Now each tool is installed at runtime into the persistent container
home by its official installer (no sudo), the same way the Docker
image itself is built on first use. Self-updates work inside the
container and survive image rebuilds and container recreation. The
image shrinks to a plain Arch base, and agent-container update no
longer removes containers, so sudo-installed project dependencies
survive it too.
docker exec now attaches a TTY only when stdin is one, so scripted
runs like 'pi -p' work without a terminal.
- Install @earendil-works/pi-coding-agent globally via npm in the
image so it survives the home directory bind mount
- Add pi subcommand forwarding PI_* and provider key prefixes
- Add pi shell wrapper alias
- Build with --network host so image builds work on hosts where the
Docker bridge network cannot create veth pairs
Replace hardcoded env var lists with prefix-based forwarding (OPENCODE_*,
ANTHROPIC_*, CLAUDE_*) so new env vars are picked up automatically.
Run tools through bash -l inside the container so that .bashrc, .bash_profile,
and .profile from container-home are sourced. Seed container-home with default
shell config from /etc/skel on first run if the files don't already exist.
Use --network host so that web interfaces (e.g. opencode web) are
accessible from the host. Suppress Python tracebacks when the tool
exits with a non-zero code. Ignore SIGTSTP to prevent Ctrl+Z from
leaving orphaned containers. Rename force-cleanup to purge.
Merge the two separate container projects into a single image and
management script. Both OpenCode and Claude Code are installed in the
same Arch Linux image and share one persistent $HOME directory, which
enables the opencode-claude-bridge plugin to read Claude CLI credentials
from within OpenCode.
Subcommands: opencode, claude, update, force-cleanup.