Containers used `sleep infinity` as PID 1, which never reacts to
SIGTERM: the kernel does not apply default signal dispositions to PID 1,
and sleep has no SIGTERM handler, so every `podman stop` silently
ignored the signal, waited out the full 10-second grace period, and fell
back to SIGKILL.
PID 1 now traps TERM and INT itself and exits on request, so one-off
manual stops of a still-running agent container end immediately instead
of after the kill timeout.
The PID 1 command is baked in at create time, so containers predating
this change are detected and recreated automatically on the next run.
Podman only resolves short image names through an unqualified search
registry, which is not configured by default on Arch (and on Ubuntu
setups without a registries.d file), so builds fail at the first step
with 'short-name "archlinux" did not resolve'. Naming the registry
explicitly makes the image build on any distro without per-distro
configuration.
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.
The upgrade swaps the runtime under the user's feet, and nothing reuses
the old Docker containers or image: storage moves to
~/.local/share/containers, the image has to be rebuilt, and hosts
without subuid/subgid mappings fail on the first run. Without notes that
say so, the empty container list looks like data loss and the first
failed start looks like a broken release.
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.
Tools and skills running inside the container need to know they are not
on a bare host: the install-in-container skill hard-gates on this env
var before using passwordless sudo to install packages. Env vars are
fixed at create time, so containers created before the marker existed
keep running without it and detection fails forever. Recreate those
containers on startup, mirroring the existing image-mismatch check.
A failing official installer aborted with a raw CalledProcessError
traceback, burying the actual problem the installer had already printed
to the terminal. Exit with a short message pointing at that output
instead.
Rebuilding the image no longer removes existing containers, and nothing
checked whether a container still matched the current image, so stale
containers kept running old builds. This surfaced when the pi installer
refused to install into the container home because the old image's
root-owned, system-wide pi still shadowed it.
Compare image IDs before reuse and recreate the container when they
differ, logging why.
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.
The opencode, claude, and pi functions wrap the tools in the Docker
container. Add opencode-local, claude-local, and pi-local so the real
host binaries can be invoked directly when container isolation is not
needed. They use `command` to bypass the wrapping functions.
- 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.
Add OPENCODE_SERVER_PASSWORD, OPENCODE_SERVER_USERNAME,
OPENCODE_CONFIG, OPENCODE_CONFIG_DIR, OPENCODE_CONFIG_CONTENT,
and OPENCODE_TUI_CONFIG to the environment passthrough list.
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.