When you’re using a commercial harness like Codex CLI or Claude Code CLI, most of the setup is already done for you, with the harness doing the plumbing behind the scenes. However, this is not the case when using an open-source harness like Pi, which is very minimalist by design. Pi needs a lot more configuration up-front, and one of the main issues is the lack of a build-in sandbox. In this article we’re going to address this using two separate approaches, each with its own benefits and drawbacks.

Bubblewrap

Bubblewrap is a relatively lightweight tool used by projects such as Flatpak for constructing sandbox environments. Bubblewrap is not a complete, ready-made solution with a specific security policy. Instead, the security policy is left for the user to figure out. The level of protection between the sandboxed processes and the host system is entirely determined by the arguments passed to bubblewrap.

In essence, what it does is quite similar to Docker or Podman: it runs applications inside isolated namespaces. However, unlike Podman, it doesn’t necessarily use OCI images; instead, it assembles the sandbox environment from an existing filesystem.

For example, with Bubblewrap, you might mount your real /usr read-only inside the sandbox so that you can use the host’s installed applications. The same goes for /lib, /etc, /opt and others.

This is, of course, less than ideal when you want to make sure that absolutely no state from the host machine leaks into the sandbox.
I.e. a sandbox like that will prevent Claude from committing crimes against your local filesystem, but it won’t stop it from reading — and potentially exfiltrating — secrets it finds in /etc.

Bubblewrap does have several major benefits, however: almost zero setup, no OCI image to download, it works much the same way on pretty much every Linux distribution, and it does offer real protection by isolating coding agents. Depending on your risk profile, this might be perfectly acceptable.

The following script is one that I’ve found very usable. This can, of course, be adapted to your own needs; additional mounts can be added or removed.

I’ve named it pi-bwrap, but you can call it clanker-jail if you’d like:

#!/bin/bash
set -euo pipefail

PROJECT="${1:-}"

if [[ -z "$PROJECT" ]]; then
    printf 'Usage: %s PROJECT_DIR\n' "${0##*/}" >&2
    exit 2
fi

[[ -d "$PROJECT" ]] || {
    printf 'error: PROJECT_DIR must be an existing directory: %s\n' "$PROJECT" >&2
    exit 1
}

PROJECT_PATH=$(realpath -e "$PROJECT")
PROJECT_NAME=$(basename "$PROJECT")

SANDBOX_HOME="/home/pi"
PROJECT_HOME="/workspace/$PROJECT_NAME"

HOST_DIR="$HOME/.pi-sandbox"
mkdir -p "$HOST_DIR" "$HOST_DIR/.config"

echo "Mounting $PROJECT_PATH to $PROJECT_HOME"

exec bwrap \
    --ro-bind /usr /usr \
    --ro-bind /bin /bin \
    --ro-bind /lib /lib \
    --ro-bind /lib64 /lib64 \
    --ro-bind /etc /etc \
    --ro-bind /opt /opt \
    --ro-bind /run/systemd/resolve /run/systemd/resolve \
    --ro-bind /var/cache/fontconfig /var/cache/fontconfig \
    --proc /proc \
    --dev /dev \
    --bind "$HOST_DIR" "$SANDBOX_HOME" \
    --bind "$PROJECT_PATH" "$PROJECT_HOME" \
    --bind "$HOME/.pi" "$SANDBOX_HOME/.pi" \
    --ro-bind "$XDG_CONFIG_HOME/git" "$SANDBOX_HOME/.config/git" \
    --ro-bind "$XDG_CONFIG_HOME/tmux" "$SANDBOX_HOME/.config/tmux" \
    --chdir "$PROJECT_HOME" \
    --tmpfs /tmp \
    --unshare-all \
    --share-net \
    --die-with-parent \
    --clearenv \
    --setenv HOME "$SANDBOX_HOME" \
    --setenv SHELL "$SHELL" \
    --setenv TERM "${TERM:-xterm-256color}" \
    --setenv PATH /usr/local/bin:/usr/bin:/bin \
    -- "$SHELL" -c "tmux new -A -s sandbox"

Tools like tmux, git and pi itself need to be available inside the sandbox. The script is straightforward to run with:

pi-bwrap project_folder/

From there, the developer launches pi manually. I prefer this approach rather than starting Pi directly from the script.

The git configuration assumes that multiple git profiles are used via the includeIf directive, e.g.

FILE: ~/.config/git/config

[includeIf "gitdir:/workspace/"]
    path = ~/.config/git/pi.config
FILE: ~/.config/git/pi.config

[user]
    name = Qwen
    email = Qwen@localhost
[commit]
    gpgsign = false

Inside the sandbox, bwrap sets up a minimal PID 1 to reap child processes, which is exactly what we want. The sandbox starts instantly and runs smoothly. All the tools exposed from the host are immediately available inside the sandbox, with no separate installation required. The network is shared with the host, but filesystem access is limited to the paths we’ve explicitly exposed above.

A rogue agent would not be happy being locked up like this.
But what if we want much stronger isolation from the host? This is where Podman comes in.

Enter Podman

Compared with the Bubblewrap setup above, Podman offers a stricter boundary between the host system and the container. Podman normally runs the agent from an OCI image, giving it a separate userspace and root filesystem — effectively a small, self-contained Linux environment.

However, the setup process here is a bit more involved than before. The image needs to be downloaded, customised and built ahead of time.
In this case we’re going to use the official Arch Linux OCI image published on Docker Hub.
I would run this first to ensure that podman works correctly:

podman pull docker.io/library/archlinux:base-devel

The image then needs to be customised according to taste. All applications needed by the agent have to be installed into this Arch Linux image, since otherwise they won’t be available inside the container. This is one of the major differences from the Bubblewrap approach, where installing a new application doesn’t require rebuilding an image.

FROM docker.io/library/archlinux:base-devel

RUN pacman-key --init && pacman-key --populate archlinux

RUN printf '\n[radus]\nSigLevel = Never\nServer = https://bigiron.local/packages/\n' >> /etc/pacman.conf

# System apps
RUN pacman -Syu --noconfirm \
    bat curl fd fish git git-delta htop iproute2 jq less lynx net-tools nodejs npm \
    procps-ng procs python python-pip python-uv ripgrep ruff tig tmux ty unzip uv \
    && pacman -Scc --noconfirm

# Third party apps available from radus repo
RUN pacman -Syu --noconfirm \
    pi \
    && pacman -Scc --noconfirm

WORKDIR /workspace

CMD ["fish"]

Then we need to build and tag the resulting image so that we can reuse it in our runner script.

podman build -t pi-sandbox -f Containerfile

The following script is very similar to the first one, with only the bits after exec varying.
I’ve named this one pi-sandbox, but you can call it clanker-de-montecristo if you’d like:

#!/bin/bash
set -euo pipefail

PROJECT="${1:-}"

if [[ -z "$PROJECT" ]]; then
    printf 'Usage: %s PROJECT_DIR\n' "${0##*/}" >&2
    exit 2
fi

[[ -d "$PROJECT" ]] || {
    printf 'error: PROJECT_DIR must be an existing directory: %s\n' "$PROJECT" >&2
    exit 1
}

PROJECT_PATH=$(realpath -e "$PROJECT")
PROJECT_NAME=$(basename "$PROJECT")

SANDBOX_HOME="/home/pi"
PROJECT_HOME="/workspace/$PROJECT_NAME"

HOST_DIR="$HOME/.pi-sandbox"
mkdir -p "$HOST_DIR" "$HOST_DIR/.config"

echo "Mounting $PROJECT_PATH to $PROJECT_HOME"

exec podman run \
    --rm \
    -it \
    --name pi-sandbox \
    --hostname pi-sandbox \
    --userns=keep-id \
    --cap-drop=all \
    --security-opt=no-new-privileges \
    --read-only \
    --network=pasta:-T,8080 \
    --tmpfs /tmp \
    --tmpfs /run \
    --env HOME="$SANDBOX_HOME" \
    --env SHELL=/usr/bin/fish \
    --env TERM="${TERM:-xterm-256color}" \
    --volume "$HOST_DIR:$SANDBOX_HOME:rw" \
    --volume "$PROJECT_PATH:$PROJECT_HOME:rw" \
    --volume "$HOME/.pi:$SANDBOX_HOME/.pi:rw" \
    --volume "$XDG_CONFIG_HOME/git:$SANDBOX_HOME/.config/git:ro" \
    --volume "$XDG_CONFIG_HOME/tmux:$SANDBOX_HOME/.config/tmux:ro" \
    --workdir "$PROJECT_HOME" \
    pi-sandbox \
    fish -c 'tmux new -A -s sandbox'

The major difference from a security perspective is that the coding agent now gets its own container filesystem and userspace, rather than a filtered view of the host.

Apart from forwarding port 8080 so that the container can reach my local llama-server, the agent isn’t given general access to services listening on the host.

A rogue agent would not be happy at all being locked up in this contraption.

Here is a quick recap of the benefits of each approach:

Recap

The two scripts are aiming at the same practical outcome, but they isolate in different ways. The biggest conceptual difference is this:

  • bwrap gives the agent a restricted view of your existing host system.
  • Podman gives the agent a separate container filesystem and userspace that you explicitly populate.

bwrap advantages

The bwrap version is very lightweight. It essentially says:
Take my current machine, hide almost everything, make system directories read-only, expose these two writable directories, and run Pi.

This means there is almost nothing to maintain. If you update: fish, git, node, python, ripgrep, pi, tmux on the host, the sandbox immediately sees those new versions. There is no image rebuild.

Startup is also basically instantaneous. bwrap is mostly setting up namespaces and bind mounts and then executing the program.

For a local llama-server setup, the --share-net directive is particularly convenient, as Pi sees the host network namespace, and in turn can access localhost directly.

bwrap disadvantages

The flip side is that Pi can see quite a lot of your host.
We’re exposing: /usr, /bin, /lib, /lib64, /etc, /opt as read-only. That prevents modification, but it does not prevent inspection. For example, depending on what’s in /etc, the agent could potentially read secrets like:

/etc/shadow
/etc/ssh/
/etc/wireguard/

and any application-specific configuration you happen to have under /etc.

Likewise, /opt may contain things unrelated to Pi. This isn’t necessarily dangerous, but it makes the boundary broader than it needs to be. There’s also less environmental reproducibility. Your Pi sandbox tomorrow is whatever your Linux system happens to contain tomorrow.

If a host upgrade changes:

Python 3.13 → 3.14
Node 24 → 25
git behaviour
glibc
Pi version

the sandbox changes with it. That can actually be an advantage for personal use, but it’s a disadvantage when you want a stable agent environment.

The network isolation is also fairly weak. The agent shares your entire host network namespace. It can potentially access anything you’re listening on locally. That’s probably the most significant security difference between your two setups.

Podman advantages

Podman gives you a much stronger environment boundary. Pi sees the Arch Linux image you built, these are the container’s files, not the host’s. So if your image contains only: fish, tmux, git, python, node, ripgrep, pi, that’s essentially all Pi has available.

The host /etc, /usr, /opt, package database, services, and miscellaneous software are invisible unless you explicitly mount them.

The OCI image also gives you reproducibility, and the ability to know exactly what environment Pi gets regardless of what you’ve installed on the workstation.

The networking setup is also more attractive from a sandboxing standpoint with the directive
--network=pasta:-T,8080. Pi gets Internet access, but you’re not simply dropping it into the host’s network namespace. And you can deliberately expose the local service you need rather than exposing every host-loopback service.

We also have:

--cap-drop=all
--security-opt=no-new-privileges
--read-only

which makes the environment fairly constrained.

Podman disadvantages

The main cost is complexity. You now own an image. If you want a new version of PI 0.85.1 → 0.86.0 you have to rebuild it. If Pi suddenly needs gcc, cmake, go, rust, sqlite, or imagemagick, you either rebuild the image or temporarily install packages. This matters quite a lot for coding agents. They often encounter a project and decide:

I need pytest
I need npm
I need gcc
I need go

With the bwrap setup, if it’s on the host, it’s immediately available. With the root as read-only in Podman, the agent can’t even run pacman -S pkg at runtime. So there’s slightly more moving machinery versus bwrap’s fairly direct namespace setup.