{"slug": "isolated-ai-coding-agent-on-linux-design-template-podman-overlayfs-bindfs-git", "title": "Isolated AI coding agent on Linux design template (Podman, overlayfs, bindfs, git)", "summary": "A developer published a reference architecture for running an AI coding agent in an isolated rootless Podman container on Linux, using overlayfs and bindfs to give the agent read-only access to the host home directory while confining all writes to a single git-versioned work directory. The design runs the agent as a dedicated system user (pi-agent) with host mounts performed by root via /etc/fstab, and routes model calls through a local gateway over a VPN to DeepSeek. Promotion of agent changes to the real home directory and any /etc modifications remain human-controlled, with etckeeper recording system configuration changes.", "body_md": "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.\n\nDesign template. Adapt values, paths, and tool choices to the target environment.\n\n- A Linux distribution with systemd and standard Unix permission semantics (Debian 13 used as reference).\n- Podman, rootless, no daemon.\n- overlayfs and FUSE/bindfs available.\n- `fuse-overlayfs` and`bindfs` installed.\n- The `fuse` package (providing`fusermount` ) installed, required for FUSE\nmounts via`/etc/fstab` .\n- yadm (or an equivalent dotfiles manager) for the host home repo.\n- A local LLM gateway reachable over a VPN interface.\n- `etckeeper` available for system configuration versioning.\n\n```\nPi agent (Podman container, --network=host, host user: pi-agent)\n   |  network -> Bifrost (wg0 VPN) -> DeepSeek\n   v  writes into /work\n/srv/pi/work  (overlayfs merged, the only directory mounted into the container)\n   |- overlayfs:\n   |    lowerdir = host home (read-only)\n   |    upperdir = /srv/pi/.overlay/upper (writable, agent changes)\n   |    workdir  = /srv/pi/.overlay/work (kernel scratch)\n   |- git repo: versioned agent changes\n   |- controlled promotion (test branch -> merge)\n   v\nHost home (yadm, umask 007, .gitignore excludes secrets and Work)\n   ^\n   bindfs (fstab, root) -> ~/Work (host user transparent access)\n\n/etc (system configuration, versioned with etckeeper)\n   ^  human applies agent-proposed changes with sudo\n```\n\nProperties:\n\n- The agent writes only into `/srv/pi/work` (the overlay merged layer).\n- The host home is the overlay lowerdir, read-only. The agent reads it without duplicating data and cannot write to it.\n- The agent runs as a separate user/group (`pi-agent` ), so Unix permissions\nprotect host files.\n- Only `work` is mounted into the container. The agent cannot see`.overlay` ,\nthe host home, or any host paths.\n- Changes are versioned in git (work). The overlay layer isolates the agent's writes from the host home.\n- Promotion to the real home is human-controlled.\n- System configuration (`/etc` ) is never written by the agent directly; the\nagent proposes, the human applies with sudo, and`etckeeper` records it.\n- All mounts (overlay and bindfs) are performed by root via `/etc/fstab` .\nThe host user lacks ownership of`/srv/pi` , and`bindfs` UID remapping\nrequires root; therefore no user mount units are used.\n\n| Item | Configuration | \n|---|---|\n| Agent | Pi (Mario Zechner terminal harness) | \n| Runtime | Podman, rootless, no daemon | \n| Network | `--network=host` | \n| Host user | `pi-agent` , group`pi-agent` , system user | \n\nInside the container:\n\n| Path | Role | \n|---|---|\n| `/home/pi` | Agent home (config, caches) - ephemeral | \n| `/work` | Work directory, mounted to `/srv/pi/work` | \n\nOnly `/work` is mounted into the container. `/home/pi` is ephemeral; the agent\ndoes not retain config or caches between sessions. The agent has no access to\n`.overlay`, the host home, or host filesystem paths.\n\nLaunch (overlay mode):\n\n```\npodman run --rm -it --network=host \\\n  --env HOME=/home/pi \\\n  --volume /srv/pi/work:/work:Z \\\n  imagen-pi\n```\n\n**System user (UID < 1000) decision:**\n\nThe agent is a system user (`pi-agent`): it runs as a service rather than an\ninteractive account, and its dedicated group reinforces permission separation\nfrom the host user. Adjustments required:\n\n- Assign a usable login shell (system users default to `nologin` /`false` ).\n- Add explicit user-namespace ranges for rootless Podman in `/etc/subuid` and`/etc/subgid` :\n\n```\npi-agent:100000:65536\n```\n\nLaunch as the agent user with the runtime dir set:\n\n```\nsudo -u pi-agent XDG_RUNTIME_DIR=/run/user/$(id -u pi-agent) podman ...\n```\n\n**Critical requirement - host home protection:**\n\nThe agent MUST run as the `pi-agent` user, never as the host user's UID.\nRunning the container with the host user's UID invalidates permission\nprotection: the agent becomes the owner of host files and umask 007 no longer\napplies. The overlay lowerdir read-only mount prevents writes but not reads if\nthe agent shares the host UID.\n\n**Containerfile (from official Pi plain-Docker pattern):**\n\n```\nFROM node:24-bookworm-slim\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\nRUN useradd -m -s /bin/bash -d /home/pi pi && chown -R pi:pi /home/pi\nWORKDIR /work\nENV HOME=/home/pi\nCOPY pi-config/ /home/pi/.pi/agent/\nUSER pi\nENTRYPOINT [\"pi\"]\n```\n\nThe `pi-config/` directory holds the agent configuration (Bifrost endpoint as\nan OpenAI-compatible provider). Credentials are injected at runtime, not\ncommitted to the image.\n\n| Item | Configuration | \n|---|---|\n| Gateway | Bifrost (local) | \n| Access | Reachable via `wg0` VPN interface | \n| Models | DeepSeek | \n| Agent link | OpenAI-compatible endpoint | \n| Network | `--network=host` ; proxy available as an outbound control point | \n\nThe upstream model credential lives only in the gateway. The agent talks to the gateway and never sees the upstream key.\n\n| Item | Configuration | \n|---|---|\n| Work (merged) | `/srv/pi/work` | \n| overlayfs lowerdir | Host home, read-only | \n| overlayfs upperdir | `/srv/pi/.overlay/upper` , owned by`pi-agent` | \n| overlayfs workdir | `/srv/pi/.overlay/work` | \n| Host access | `bindfs` ->`~/Work` (transparent) | \n\n`upperdir` and `workdir` must not reside inside the `lowerdir`. Placing them\nunder `/srv/pi/.overlay` (outside the host home) avoids overlay recursion.\n\n**Transient overlay - per-session lifecycle:**\n\nThe overlay layer is disposable and recreated for each session to avoid divergence between the host home (lowerdir) and stale upperdir data.\n\n- Session start: discard the previous upperdir, mount a fresh overlay against the current host home.\n- During the session: the agent reads the host home and writes to the fresh upperdir.\n- Session end: promote approved changes to the host home via git, then discard the upperdir.\n\nDo 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.\n\n**Safe reset procedure:**\n\nBefore remounting for a new session, discard the upperdir without risking the host home. Use a script that rejects unsafe paths and deletes only content:\n\n``` bash\n#!/bin/bash\nset -euo pipefail\n\nUPPERDIR=\"/srv/pi/.overlay/upper\"\nWORKDIR=\"/srv/pi/.overlay/work\"\n\n# Safety check: reject unsafe paths\nfor d in \"$UPPERDIR\" \"$WORKDIR\"; do\n  if [ \"$d\" = \"$HOME\" ] || [ \"$d\" = \"/\" ] || [ -z \"$d\" ]; then\n    echo \"Refusing to discard: unsafe path ($d)\" >&2\n    exit 1\n  fi\ndone\n\n# Verify the agent is stopped\nif pgrep -u pi-agent -f \"podman\" >/dev/null 2>&1; then\n  echo \"Agent still running; stop it before discarding\" >&2\n  exit 1\nfi\n\n# Delete only content, never the root directory\nfind \"$UPPERDIR\" -mindepth 1 -delete\nfind \"$WORKDIR\" -mindepth 1 -delete\n\necho \"Overlay layer discarded safely.\"\n```\n\nThe reset flow is: stop the agent, promote approved changes, unmount the overlay, run the reset script, remount, relaunch.\n\nThe same image supports two modes of use. The only difference is what is\nmounted at `/work`.\n\n**Mode 1 - Overlay (home-aware):** the agent sees the host home read-only via\nthe overlay and writes into `/srv/pi/work`.\n\n```\npodman run --rm -it --network=host \\\n  --env HOME=/home/pi \\\n  --volume /srv/pi/work:/work:Z \\\n  imagen-pi\n```\n\n**Mode 2 - Project (repository):** the agent is limited to a single host\ndirectory (a git repository) mounted at `/work`. The host home and its secrets\nare entirely outside the container. This provides maximum isolation and is\nsimpler than the overlay, since no overlayfs is involved. Reversion is handled\nby git in the repository.\n\n```\npodman run --rm -it --network=host \\\n  --env HOME=/home/pi \\\n  --volume /home/TU_USUARIO/proyectos/mi-repo:/work:Z \\\n  imagen-pi\n```\n\nUse Mode 2 for concrete tasks on individual repositories; use Mode 1 when the\nagent must read the host home/global config while writing only into `work`.\n\nBoth mounts are performed by root via `/etc/fstab`. The host user does not own\n`/srv/pi`, and `bindfs` UID remapping requires root, so no user mount units are\nused.\n\n```\n# 1. Overlay (fuse-overlayfs)\n/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\n\n# 2. Bindfs (options comma-separated; map without leading --)\n/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\n```\n\nRequirements:\n\n- The `fuse` package (providing`fusermount` ) must be installed for FUSE\nmounts via fstab.\n- In fstab, options are comma-separated. The bindfs mapping option is `map=` ,\nnot`--map=` .\n- If root mounts the bindfs and the host user accesses `~/Work` , set`user_allow_other` in`/etc/fuse.conf` .\n- Mount order: overlay first (`/srv/pi/work` ), bindfs second (`~/Work` ),\nenforced with`x-systemd.requires` /`x-systemd.after` .\n\nThe agent must never write directly to `/etc`. System configuration is handled\nthrough a propose-and-apply flow with `etckeeper`:\n\n```\n1. The agent proposes a /etc change as a draft in work.\n2. The human reviews the draft.\n3. The human applies the change to /etc with sudo.\n4. etckeeper records the change (automatic commit with diff).\n5. If a change breaks the system, revert with git in /etc.\n```\n\n`etckeeper` versions `/etc` with git and hooks into the package manager\n(`apt`/` dpkg` on Debian), committing the state before and after package\noperations.\n\nSetup (Debian):\n\n```\nsudo apt install etckeeper\nsudo etckeeper init\n```\n\nNotes:\n\n- `etckeeper init` creates a baseline commit of the current`/etc` without\ndeleting anything; existing configuration becomes the starting point.\n- `etckeeper` excludes sensitive files by default (`/etc/shadow` ,`/etc/gshadow` , private keys). Verify exclusions on setup.\n- The `/etc` git repository may be exposed through the overlay if the host home\nis the lowerdir; protect or relocate it.\n\n| Scope | Tool | Notes | \n|---|---|---|\n| Host home | yadm | Git-based dotfiles management | \n| Work ( `/srv/pi/work` ) | git | Separate repo for agent changes | \n| `/etc` | etckeeper | System configuration, hook-driven | \n| yadm `.gitignore` | - | Exclude `Work` and all secrets | \n\nThe 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.\n\n```\n1. Agent commits changes in work.\n2. Reviewer inspects the diff.\n3. Promote to a test branch on the home repo.\n4. Validate on the branch.\n5. Merge to the default branch, or apply a selective rsync.\n```\n\nThe human approves every promotion. Nothing moves from work to the home automatically. Promote before discarding the overlay layer.\n\n| Item | Configuration | \n|---|---|\n| umask | `007` (user scope): new files/dirs 660/770 | \n| Agent user | Different group ( `pi-agent` ), falls under \"others\" | \n| `.ssh` | Restricted (600/700) | \n| overlayfs | Respects permissions | \n\numask 007 protects against the `pi-agent` user only while that user is not a\nmember of a group with read access. It is not retroactive; existing files with\n`others` read bits require a one-time hardening pass.\n\nHost 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.\n\n- Secrets are never committed to git (yadm, work, or /etc via etckeeper).\n- Command allowlist/denylist restricts the agent.\n- Host network is accepted; a proxy can act as an optional outbound allowlist.\n- Agent logs are reviewed to detect prompt injection or anomalous behavior.\n\n| Layer | Purpose | \n|---|---|\n| overlayfs | Discard the layer for a per-session reset | \n| git (work) | Fine-grained revert of changes | \n| etckeeper | Revert of `/etc` changes | \n| restic / rsnapshot | Cover binaries, caches, non-text data | \n| External | Off-machine copies; same-disk snapshots insufficient | \n\n- Containerfile for the agent, versioned in yadm.\n- fstab entries for the overlay and the bindfs.\n- `etckeeper` config/state for`/etc` .\n\n| Decision | Choice | Considered alternatives | \n|---|---|---|\n| Workspace isolation | overlayfs | btrfs snapshots, container volume, plain dir | \n| Home exposure | bindfs --map | symlink, `mount --bind` | \n| Default umask | 007 | 077 (owner-only), 022 (group-read) | \n| Dotfiles manager | yadm | chezmoi, GNU Stow, plain git | \n| Container runtime | Podman | Docker, systemd-nspawn | \n| Sandboxing | container | bubblewrap, firejail, Landlock | \n| Agent account type | system user | regular user (UID >= 1000) | \n| Mount location | fstab (root) | user mount units | \n| System config | etckeeper | NixOS declarative config | \n\nRationale:\n\n- overlayfs exposes the home read-only and stores only deltas.\n- bindfs --map resolves UID mismatch while keeping underlying files owned by\n`pi-agent` .\n- umask 007 permits group sharing while excluding \"others\"; 077 is stricter.\n- Podman is rootless and daemon-less, matching a minimal setup.\n- A system user models the agent as a service; the tradeoff is subuid/subgid setup.\n- Both mounts live in fstab: the host user does not own `/srv/pi` , and bindfs\nUID remapping requires root. User mount units cannot remap owners, so fstab\n(root) is required.\n- Mode 2 (project/repository) provides maximum isolation with no overlayfs and git-only reversion; Mode 1 (overlay) provides home-aware context when needed.\n- `etckeeper` versions`/etc` declaratively and integrates with the package\nmanager, keeping the agent out of system paths.\n\n- Overlay recursion: upperdir/workdir must not reside inside lowerdir.\n- Whiteouts: an agent can create deletion markers in the upperdir, hiding files in its view without modifying the host home.\n- UID mismatch: a bindfs --map that remaps host files to the agent user's UID negates permission protection.\n- Exposed yadm repo: its git history is readable through the overlay if not protected or relocated.\n- Non-filesystem vectors: /proc, /sys, environment, network are not covered by file permissions.\n- umask is not retroactive: existing permissive files require a separate pass.\n- Rootless Podman as a system user (UID < 1000) requires explicit subuid/subgid ranges.\n- Launching the container with the host user's UID invalidates permission protection on the host home.\n- A system user defaulting to a non-login shell must be assigned a usable shell.\n- 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.\n- The `fuse` package must be installed for FUSE mounts via fstab; otherwise the\nmount fails at boot even if the CLI mount works.\n- In fstab, bindfs options are comma-separated and use `map=` (no leading`--` ).\n- If root mounts the bindfs and the host user accesses it, `user_allow_other` must be set in`/etc/fuse.conf` .\n- Mode 2 mounts a host directory directly; ensure it contains no secrets and that only the intended repository is exposed.\n- The `etckeeper` repository lives inside`/etc` and may hold sensitive\nconfiguration history; verify exclusions and protect access.\n\n1. Create the `pi-agent` user/group; assign a home outside the host home.\n2. Assign a usable login shell to `pi-agent` .\n3. Add subuid/subgid ranges for `pi-agent` in`/etc/subuid` and`/etc/subgid` .\n4. Install the agent in the container; wire it to the gateway.\n5. Set `umask 007` in the shell profile (user scope).\n6. Harden existing files with `others` read bits (one-time pass).\n7. Create `/srv/pi/work` ,`/srv/pi/.overlay/upper` ,`/srv/pi/.overlay/work` .\n8. Initialize the git repo in `/srv/pi/work` .\n9. Install `fuse` ,`fuse-overlayfs` ,`bindfs` ; set`user_allow_other` in`/etc/fuse.conf` if required.\n10. Add the overlay and bindfs entries to `/etc/fstab` ; run`mount -a` .\n11. Exclude `Work` and secrets from the dotfiles repo.\n12. Define the agent command allowlist/denylist.\n13. Configure backups and external copies.\n14. Test the full flow: agent edits in work, diff, branch, merge.\n15. Always launch the agent as `pi-agent` , never the host user's UID.\n16. Discard and remount the overlay at each session start; promote before discarding.\n17. Install and initialize `etckeeper` ; verify its sensitive-file exclusions.\n18. Apply agent-proposed `/etc` changes manually with sudo; let etckeeper record\nthem.", "url": "https://wpnews.pro/news/isolated-ai-coding-agent-on-linux-design-template-podman-overlayfs-bindfs-git", "canonical_source": "https://gist.github.com/amartinr/5866cb0a6374ac217cefef3af1780f09", "published_at": "2026-08-29 09:42:12+00:00", "updated_at": "2026-09-27 16:01:07.480969+00:00", "lang": "en", "topics": ["ai-agents", "ai-infrastructure", "developer-tools", "mlops", "ai-tools"], "entities": ["Podman", "overlayfs", "bindfs", "DeepSeek", "yadm", "etckeeper", "Mario Zechner", "Debian"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/isolated-ai-coding-agent-on-linux-design-template-podman-overlayfs-bindfs-git", "markdown": "https://wpnews.pro/news/isolated-ai-coding-agent-on-linux-design-template-podman-overlayfs-bindfs-git.md", "text": "https://wpnews.pro/news/isolated-ai-coding-agent-on-linux-design-template-podman-overlayfs-bindfs-git.txt", "jsonld": "https://wpnews.pro/news/isolated-ai-coding-agent-on-linux-design-template-podman-overlayfs-bindfs-git.jsonld"}}