Reference architecture for running a coding agent in an isolated container with controlled read/write access to a host home directory, versioned via git, and routed through a local gateway to remote models.
Design template. Adapt values, paths, and tool choices to the target environment.
- A Linux distribution with systemd and standard Unix permission semantics (Debian 13 used as reference).
- Podman, rootless, no daemon.
- overlayfs and FUSE/bindfs available.
fuse-overlayfsandbindfsinstalled.- The
fusepackage (providingfusermount) installed, required for FUSE mounts via/etc/fstab. - yadm (or an equivalent dotfiles manager) for the host home repo.
- A local LLM gateway reachable over a VPN interface.
etckeeperavailable for system configuration versioning.
Pi agent (Podman container, --network=host, host user: pi-agent)
| network -> Bifrost (wg0 VPN) -> DeepSeek
v writes into /work
/srv/pi/work (overlayfs merged, the only directory mounted into the container)
|- overlayfs:
| lowerdir = host home (read-only)
| upperdir = /srv/pi/.overlay/upper (writable, agent changes)
| workdir = /srv/pi/.overlay/work (kernel scratch)
|- git repo: versioned agent changes
|- controlled promotion (test branch -> merge)
v
Host home (yadm, umask 007, .gitignore excludes secrets and Work)
^
bindfs (fstab, root) -> ~/Work (host user transparent access)
/etc (system configuration, versioned with etckeeper)
^ human applies agent-proposed changes with sudo
Properties:
- The agent writes only into
/srv/pi/work(the overlay merged layer). - The host home is the overlay lowerdir, read-only. The agent reads it without duplicating data and cannot write to it.
- The agent runs as a separate user/group (
pi-agent), so Unix permissions protect host files. - Only
workis mounted into the container. The agent cannot see.overlay, the host home, or any host paths. - Changes are versioned in git (work). The overlay layer isolates the agent's writes from the host home.
- Promotion to the real home is human-controlled.
- System configuration (
/etc) is never written by the agent directly; the agent proposes, the human applies with sudo, andetckeeperrecords it. - All mounts (overlay and bindfs) are performed by root via
/etc/fstab. The host user lacks ownership of/srv/pi, andbindfsUID remapping requires root; therefore no user mount units are used.
| Item | Configuration |
|---|---|
| Agent | Pi (Mario Zechner terminal harness) |
| Runtime | Podman, rootless, no daemon |
| Network | --network=host |
| Host user | pi-agent , grouppi-agent , system user |
Inside the container:
| Path | Role |
|---|---|
/home/pi |
Agent home (config, caches) - ephemeral |
/work |
Work directory, mounted to /srv/pi/work |
Only /work is mounted into the container. /home/pi is ephemeral; the agent
does not retain config or caches between sessions. The agent has no access to
.overlay, the host home, or host filesystem paths.
Launch (overlay mode):
podman run --rm -it --network=host \
--env HOME=/home/pi \
--volume /srv/pi/work:/work:Z \
imagen-pi
System user (UID < 1000) decision:
The agent is a system user (pi-agent): it runs as a service rather than an
interactive account, and its dedicated group reinforces permission separation
from the host user. Adjustments required:
- Assign a usable login shell (system users default to
nologin/false). - Add explicit user-namespace ranges for rootless Podman in
/etc/subuidand/etc/subgid:
pi-agent:100000:65536
Launch as the agent user with the runtime dir set:
sudo -u pi-agent XDG_RUNTIME_DIR=/run/user/$(id -u pi-agent) podman ...
Critical requirement - host home protection:
The agent MUST run as the pi-agent user, never as the host user's UID.
Running the container with the host user's UID invalidates permission
protection: the agent becomes the owner of host files and umask 007 no longer
applies. The overlay lowerdir read-only mount prevents writes but not reads if
the agent shares the host UID.
Containerfile (from official Pi plain-Docker pattern):
FROM node:24-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
RUN useradd -m -s /bin/bash -d /home/pi pi && chown -R pi:pi /home/pi
WORKDIR /work
ENV HOME=/home/pi
COPY pi-config/ /home/pi/.pi/agent/
USER pi
ENTRYPOINT ["pi"]
The pi-config/ directory holds the agent configuration (Bifrost endpoint as
an OpenAI-compatible provider). Credentials are injected at runtime, not
committed to the image.
| Item | Configuration |
|---|---|
| Gateway | Bifrost (local) |
| Access | Reachable via wg0 VPN interface |
| Models | DeepSeek |
| Agent link | OpenAI-compatible endpoint |
| Network | --network=host ; proxy available as an outbound control point |
The upstream model credential lives only in the gateway. The agent talks to the gateway and never sees the upstream key.
| Item | Configuration |
|---|---|
| Work (merged) | /srv/pi/work |
| overlayfs lowerdir | Host home, read-only |
| overlayfs upperdir | /srv/pi/.overlay/upper , owned bypi-agent |
| overlayfs workdir | /srv/pi/.overlay/work |
| Host access | bindfs ->~/Work (transparent) |
upperdir and workdir must not reside inside the lowerdir. Placing them
under /srv/pi/.overlay (outside the host home) avoids overlay recursion.
Transient overlay - per-session lifecycle:
The overlay layer is disposable and recreated for each session to avoid divergence between the host home (lowerdir) and stale upperdir data.
- Session start: discard the previous upperdir, mount a fresh overlay against the current host home.
- During the session: the agent reads the host home and writes to the fresh upperdir.
- Session end: promote approved changes to the host home via git, then discard the upperdir.
Do not edit host-home files that the agent is concurrently modifying; overlayfs gives priority to the upperdir, hiding the host edit from the agent. Git resolves any conflict during promotion. Discarding the upperdir deletes uncommitted agent work; promote before discarding.
Safe reset procedure:
Before remounting for a new session, discard the upperdir without risking the host home. Use a script that rejects unsafe paths and deletes only content:
#!/bin/bash
set -euo pipefail
UPPERDIR="/srv/pi/.overlay/upper"
WORKDIR="/srv/pi/.overlay/work"
for d in "$UPPERDIR" "$WORKDIR"; do
if [ "$d" = "$HOME" ] || [ "$d" = "/" ] || [ -z "$d" ]; then
echo "Refusing to discard: unsafe path ($d)" >&2
exit 1
fi
done
if pgrep -u pi-agent -f "podman" >/dev/null 2>&1; then
echo "Agent still running; stop it before discarding" >&2
exit 1
fi
find "$UPPERDIR" -mindepth 1 -delete
find "$WORKDIR" -mindepth 1 -delete
echo "Overlay layer discarded safely."
The reset flow is: stop the agent, promote approved changes, unmount the overlay, run the reset script, remount, relaunch.
The same image supports two modes of use. The only difference is what is
mounted at /work.
Mode 1 - Overlay (home-aware): the agent sees the host home read-only via
the overlay and writes into /srv/pi/work.
podman run --rm -it --network=host \
--env HOME=/home/pi \
--volume /srv/pi/work:/work:Z \
imagen-pi
Mode 2 - Project (repository): the agent is limited to a single host
directory (a git repository) mounted at /work. The host home and its secrets
are entirely outside the container. This provides maximum isolation and is
simpler than the overlay, since no overlayfs is involved. Reversion is handled
by git in the repository.
podman run --rm -it --network=host \
--env HOME=/home/pi \
--volume /home/TU_USUARIO/proyectos/mi-repo:/work:Z \
imagen-pi
Use Mode 2 for concrete tasks on individual repositories; use Mode 1 when the
agent must read the host home/global config while writing only into work.
Both mounts are performed by root via /etc/fstab. The host user does not own
/srv/pi, and bindfs UID remapping requires root, so no user mount units are
used.
/srv/pi/.overlay/upper /srv/pi/work fuse.overlayfs lowerdir=/home/TU_USUARIO,upperdir=/srv/pi/.overlay/upper,workdir=/srv/pi/.overlay/work,x-systemd.automount 0 0
/srv/pi/work /home/TU_USUARIO/Work fuse.bindfs map=TU_USUARIO/pi-agent,x-systemd.requires=/srv/pi/work,x-systemd.after=/srv/pi/work 0 0
Requirements:
- The
fusepackage (providingfusermount) must be installed for FUSE mounts via fstab. - In fstab, options are comma-separated. The bindfs mapping option is
map=, not--map=. - If root mounts the bindfs and the host user accesses
~/Work, setuser_allow_otherin/etc/fuse.conf. - Mount order: overlay first (
/srv/pi/work), bindfs second (~/Work), enforced withx-systemd.requires/x-systemd.after.
The agent must never write directly to /etc. System configuration is handled
through a propose-and-apply flow with etckeeper:
1. The agent proposes a /etc change as a draft in work.
2. The human reviews the draft.
3. The human applies the change to /etc with sudo.
4. etckeeper records the change (automatic commit with diff).
5. If a change breaks the system, revert with git in /etc.
etckeeper versions /etc with git and hooks into the package manager
(apt/ dpkg on Debian), committing the state before and after package
operations.
Setup (Debian):
sudo apt install etckeeper
sudo etckeeper init
Notes:
etckeeper initcreates a baseline commit of the current/etcwithout deleting anything; existing configuration becomes the starting point.etckeeperexcludes sensitive files by default (/etc/shadow,/etc/gshadow, private keys). Verify exclusions on setup.- The
/etcgit repository may be exposed through the overlay if the host home is the lowerdir; protect or relocate it.
| Scope | Tool | Notes |
|---|---|---|
| Host home | yadm | Git-based dotfiles management |
Work ( /srv/pi/work ) |
git | Separate repo for agent changes |
/etc |
etckeeper | System configuration, hook-driven |
yadm .gitignore |
- | Exclude Work and all secrets |
The yadm repository lives inside the host home and is exposed through the overlay. With umask 007 its files are group-only; relocate it outside the home for additional isolation.
1. Agent commits changes in work.
2. Reviewer inspects the diff.
3. Promote to a test branch on the home repo.
4. Validate on the branch.
5. Merge to the default branch, or apply a selective rsync.
The human approves every promotion. Nothing moves from work to the home automatically. Promote before discarding the overlay layer.
| Item | Configuration |
|---|---|
| umask | 007 (user scope): new files/dirs 660/770 |
| Agent user | Different group ( pi-agent ), falls under "others" |
.ssh |
Restricted (600/700) |
| overlayfs | Respects permissions |
umask 007 protects against the pi-agent user only while that user is not a
member of a group with read access. It is not retroactive; existing files with
others read bits require a one-time hardening pass.
Host home protection is a primary requirement. All layers depend on the agent running as a separate user/group. Do not launch the agent with the host user's UID.
- Secrets are never committed to git (yadm, work, or /etc via etckeeper).
- Command allowlist/denylist restricts the agent.
- Host network is accepted; a proxy can act as an optional outbound allowlist.
- Agent logs are reviewed to detect prompt injection or anomalous behavior.
| Layer | Purpose |
|---|---|
| overlayfs | Discard the layer for a per-session reset |
| git (work) | Fine-grained revert of changes |
| etckeeper | Revert of /etc changes |
| restic / rsnapshot | Cover binaries, caches, non-text data |
| External | Off-machine copies; same-disk snapshots insufficient |
- Containerfile for the agent, versioned in yadm.
- fstab entries for the overlay and the bindfs.
etckeeperconfig/state for/etc.
| Decision | Choice | Considered alternatives |
|---|---|---|
| Workspace isolation | overlayfs | btrfs snapshots, container volume, plain dir |
| Home exposure | bindfs --map | symlink, mount --bind |
| Default umask | 007 | 077 (owner-only), 022 (group-read) |
| Dotfiles manager | yadm | chezmoi, GNU Stow, plain git |
| Container runtime | Podman | Docker, systemd-nspawn |
| Sandboxing | container | bubblewrap, firejail, Landlock |
| Agent account type | system user | regular user (UID >= 1000) |
| Mount location | fstab (root) | user mount units |
| System config | etckeeper | NixOS declarative config |
Rationale:
-
overlayfs exposes the home read-only and stores only deltas.
-
bindfs --map resolves UID mismatch while keeping underlying files owned by
pi-agent. -
umask 007 permits group sharing while excluding "others"; 077 is stricter.
-
Podman is rootless and daemon-less, matching a minimal setup.
-
A system user models the agent as a service; the tradeoff is subuid/subgid setup.
-
Both mounts live in fstab: the host user does not own
/srv/pi, and bindfs UID remapping requires root. User mount units cannot remap owners, so fstab (root) is required. -
Mode 2 (project/repository) provides maximum isolation with no overlayfs and git-only reversion; Mode 1 (overlay) provides home-aware context when needed.
-
etckeeperversions/etcdeclaratively and integrates with the package manager, keeping the agent out of system paths. -
Overlay recursion: upperdir/workdir must not reside inside lowerdir.
-
Whiteouts: an agent can create deletion markers in the upperdir, hiding files in its view without modifying the host home.
-
UID mismatch: a bindfs --map that remaps host files to the agent user's UID negates permission protection.
-
Exposed yadm repo: its git history is readable through the overlay if not protected or relocated.
-
Non-filesystem vectors: /proc, /sys, environment, network are not covered by file permissions.
-
umask is not retroactive: existing permissive files require a separate pass.
-
Rootless Podman as a system user (UID < 1000) requires explicit subuid/subgid ranges.
-
Launching the container with the host user's UID invalidates permission protection on the host home.
-
A system user defaulting to a non-login shell must be assigned a usable shell.
-
Layer divergence: editing a host-home file the agent modified in the upperdir hides the host edit from the agent (upperdir takes priority). Discard and remount per session.
-
The
fusepackage must be installed for FUSE mounts via fstab; otherwise the mount fails at boot even if the CLI mount works. -
In fstab, bindfs options are comma-separated and use
map=(no leading--). -
If root mounts the bindfs and the host user accesses it,
user_allow_othermust be set in/etc/fuse.conf. -
Mode 2 mounts a host directory directly; ensure it contains no secrets and that only the intended repository is exposed.
-
The
etckeeperrepository lives inside/etcand may hold sensitive configuration history; verify exclusions and protect access.
- Create the
pi-agentuser/group; assign a home outside the host home. - Assign a usable login shell to
pi-agent. - Add subuid/subgid ranges for
pi-agentin/etc/subuidand/etc/subgid. - Install the agent in the container; wire it to the gateway.
- Set
umask 007in the shell profile (user scope). - Harden existing files with
othersread bits (one-time pass). - Create
/srv/pi/work,/srv/pi/.overlay/upper,/srv/pi/.overlay/work. - Initialize the git repo in
/srv/pi/work. - Install
fuse,fuse-overlayfs,bindfs; setuser_allow_otherin/etc/fuse.confif required. - Add the overlay and bindfs entries to
/etc/fstab; runmount -a. - Exclude
Workand secrets from the dotfiles repo. - Define the agent command allowlist/denylist.
- Configure backups and external copies.
- Test the full flow: agent edits in work, diff, branch, merge.
- Always launch the agent as
pi-agent, never the host user's UID. - Discard and remount the overlay at each session start; promote before discarding.
- Install and initialize
etckeeper; verify its sensitive-file exclusions. - Apply agent-proposed
/etcchanges manually with sudo; let etckeeper record them.