How I Run Pi in a Docker Sandbox
I’ve been wanting to try pi.dev for a while, but I’ve been a little hesitant to run it directly on my machine due to its lack of built-in permission system and the possibility of it running a destructive command without my approval.1
Naturally I asked ChatGPT what my options were to containerize or run the agent in some kind of sandbox. To my surprise there was no consensus in the community on what the ideal approach is here. My guess is that there’s so much variance in personal preferences, security concerns or even company-mandated policies that there’s no general-purpose approach that can be recommended to everyone.
These were the requirements I had in mind while looking for an approach:
- I prefer running the software I’m developing locally, with as little indirection as possible
- I want to use the agent as a companion on my computer, and review and edit its output directly in my editor of choice
- I don’t want to allow the agent to independently install software on my computer without my explicit approval
- I don’t want the agent to access any environment variables or credentials on my machine that are unrelated to the project it’s working on
- I want the agent to be able to build, run tests and run the application it is working on. This will almost always mean that it needs to be able to run Docker containers (e.g. to run a database)
Landing on Docker Sandboxes
After searching for a bunch of approaches I found Docker Sandboxes to meet all my requirements. Docker Sandboxes are purposely built for running AI agents and their security model ensures my computer will be safe if Pi ever goes rogue.2
Pi is not one of the pre-packaged agents for Docker Sandboxes (at least at the time of writing) but it’s quite trivial to work around that by creating a custom kit. Here’s one provided by the community. The usage is very simple and that will be enough to get you started and safely experiment with Pi.
Finding the edges
The community kit is fine to experiment with the agent for a bit, but I quickly started hitting the edges of the sandbox when using it in real projects.
The main problems were:
- I wanted
miseavailable in every sandbox and not have to install it every time I created a new sandbox. - I wanted to start Pi from any subdirectory in a repo, but still mount the repo root (or else there’s no access to
.git, for example). - I wanted Pi to reuse the same config, prompts, extensions, and
AGENTS.mdacross sandboxes. - I wanted the sandbox to stay open if Pi crashed or exited, so I could debug interactively.
- I did not want to rely on the
AGENTS.mdthat Docker Sandboxes ships with, which is polluting the context with troubleshooting instructions for the sandbox itself.
So I decided to create a custom kit that addressed all of the above.
Custom kit
The kit uses the Docker variant of the shell template, to ensure Pi can run Docker containers. It installs the system packages Pi needs, installs a system Node.js to install Pi, and mise for managing project tools.
kits/pi/spec.yaml
schemaVersion: "1"
kind: agent
name: pi
displayName: pi
description: "Pi coding agent with mise, running on Docker shell sandbox image"
agent:
image: "docker/sandbox-templates:shell-docker"
# Commented out `aiFilename` to prevent this image from including a base AGENTS.md that is too wordy
# aiFilename: AGENTS.md
persistence: persistent
entrypoint:
run:
- /usr/local/bin/sbx-pi-entrypoint
# Cannot use memory because `aiFilename` is commented out
# memory:
environment:
variables:
LANG: "C.UTF-8"
LC_ALL: "C.UTF-8"
# The sandbox proxy enables Node's env proxy support, which emits a noisy
# experimental Undici warning on every npm/node invocation. Suppress only
# that warning code so other Node warnings remain visible.
NODE_OPTIONS: "--disable-warning=UNDICI-EHPA"
# Host-mounted Pi config directory. The sbx-pi wrapper mounts this path by
# default so Pi can load shared settings, AGENTS.md, extensions, skills,
# prompts, themes, etc. Override together with PI_SBX_AGENT_DIR if needed.
PI_CODING_AGENT_DIR: "/path/to/agent/pi"
commands:
startup:
- description: "Update Pi on sandbox startup"
user: "1000"
command:
- /bin/bash
- -lc
- |
set +eu
pi update
install:
- description: "Install base packages needed for mise, npm packages, and native builds"
user: "0"
command: |
set -eu
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg
install -d -m 0755 /etc/apt/keyrings
curl --fail --location --show-error --silent https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list
apt-get update && apt-get install -y --no-install-recommends \
git \
openssh-client \
build-essential \
nodejs \
fd-find \
pkg-config \
python3 \
python3-pip \
unzip \
xz-utils
if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then
ln -s /usr/bin/fdfind /usr/local/bin/fd
fi
# Keep package indexes for live/interactive sandboxes so later ad-hoc
# apt-get install commands do not require another apt-get update.
apt-get clean
- description: "Install mise"
user: "1000"
command: |
set -eu
mkdir -p /home/agent/.local/bin /home/agent/.config/mise
curl --fail --location --show-error --silent https://mise.run | sh
- description: "Install sandbox entrypoint wrapper"
user: "0"
command: |
set -eu
cat > /usr/local/bin/sbx-pi-entrypoint <<'EOF'
#!/usr/bin/env bash
set +e
# Configure Git SSH signing from the forwarded SSH agent at runtime.
# The SSH agent socket is available in interactive runs, but may not be
# available when Docker Sandboxes executes kit startup commands.
signing_key="$(ssh-add -L 2>/dev/null | head -n 1 || true)"
if [ -n "$signing_key" ]; then
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git config --global gpg.format ssh
git config --global user.signingkey "key::${signing_key}"
else
echo "No SSH public key available from ssh-add; skipping Git SSH signing config." >&2
fi
# Start Pi in the directory where sbx-pi was invoked on the host
original_pwd="${PI_SBX_ORIGINAL_PWD:-}"
if [ -n "$original_pwd" ] && [ -d "$original_pwd" ]; then
cd "$original_pwd"
fi
# Pi runs commands non-interactively, so use mise shims for the
# inherited environment. Install the active project toolset after cd so
# .tool-versions/mise.toml in the project is detected.
eval "$(/home/agent/.local/bin/mise activate bash --shims)"
mise install --yes || true
pi "$@"
status=$?
echo
echo "pi exited with status ${status}; dropping into sandbox shell."
echo "Run 'exit' to close the sandbox session."
echo
# If Pi exits and we drop into a shell inside the sandbox, put that
# shell in the same original directory too.
if [ -n "${original_pwd:-}" ] && [ -d "$original_pwd" ]; then
cd "$original_pwd"
fi
# Prefer normal PATH activation for the fallback interactive shell.
eval "$(/home/agent/.local/bin/mise activate bash)"
exec /bin/bash -l
EOF
chmod 755 /usr/local/bin/sbx-pi-entrypoint
- description: "Install Pi globally"
user: "1000"
command: |
set -eu
npm install -g @earendil-works/pi-coding-agent
pi --version || true
Pi config
I created a shared folder that holds the Pi configuration files. Every sandbox mounts it at the same absolute path, so Pi uses the same settings, prompts, extensions, and instructions no matter which project I am working on.
My AGENTS.md is intentionally short and describes the sandbox-specific assumptions:
AGENTS.md
## Environment
You run in a Docker sandbox with internet access. The workspace is mounted at its host path.
`sudo` is passwordless. Docker is available; nested containers are isolated in the microVM.
Prefer `mise` for project tooling and expose build/test/run commands as mise tasks; use `hk` for git hooks. For one-off
tools, prefer ephemeral execution, e.g. `uv run --with <pkg>`, instead of system installs.
## Work style
Ask clarifying questions only when needed to avoid likely rework.
Fix root causes rather than symptoms.
For bugs, reproduce the issue first when practical; prefer regression tests when the repo supports them.
Before finishing, verify the result; if verification is unclear, ask the user.
Do not revert unexpected user/agent changes. Work around them; if they block you, ask before stashing or reverting.
Ask before destructive actions such as deleting large paths, resetting history, or discarding changes.
## Communication
Be direct. Say when you do not know something or when a request seems wrong.
When asking for guidance, present options and a recommendation.
Use ASCII diagrams when they make complex flows clearer.
## Security
Treat secrets, tokens, credentials, private keys, and `.env` files as sensitive.
Prefer local files and official docs before internet searches. Do not pipe remote scripts to shells without inspection.
Sandbox launcher script
To make life a bit easier I created a launcher script for sandboxes that hides the complexity of creating the
sandbox, mounting the required directories, ensuring the git repository root is also mounted and that existing sandboxes
are reused. I turned this script into a shell alias (spi, as in sandboxed-pi) that I’m able to run from any directory.
sbx/bin/sbx-pi
#!/usr/bin/env bash
set -euo pipefail
# Run the pi Docker Sandbox kit from anywhere.
# Defaults to this dotfiles checkout's kit, but can be overridden with PI_SBX_KIT.
KIT="${PI_SBX_KIT:-/path/to/sbx/kits/pi}"
AGENT_NAME="${PI_SBX_NAME:-pi}"
ORIGINAL_PWD="$PWD"
# If invoked from inside a Git repository, use the repository root as the
# primary workspace so the whole repo is mounted into the sandbox. This script
# does not cd, so the host shell remains wherever sbx-pi was invoked from. The
# sandbox entrypoint cd's back to ORIGINAL_PWD before starting Pi.
if GIT_ROOT="$(git -C "$ORIGINAL_PWD" rev-parse --show-toplevel 2>/dev/null)"; then
WORKSPACE_DIR="$GIT_ROOT"
else
WORKSPACE_DIR="$ORIGINAL_PWD"
fi
PROJECT_NAME="$(basename "$WORKSPACE_DIR")"
SANDBOX_NAME="${PI_SBX_SANDBOX_NAME:-${AGENT_NAME}-${PROJECT_NAME}}"
PI_SBX_AGENT_DIR="${PI_SBX_AGENT_DIR:-/path/to/pi-agent}"
if ! command -v sbx >/dev/null 2>&1; then
echo "sbx-pi: 'sbx' command not found in PATH" >&2
exit 127
fi
if [ ! -f "$KIT/spec.yaml" ]; then
echo "sbx-pi: kit not found: $KIT" >&2
echo "Set PI_SBX_KIT to the directory containing spec.yaml." >&2
exit 1
fi
args=("$AGENT_NAME" --kit "$KIT")
# Keep the Git repository root as the primary workspace when called from inside
# a repo, otherwise keep the caller's current directory. Extra workspaces follow
# it, so the sandbox starts in the project rather than in the shared Pi config
# directory.
args+=("$WORKSPACE_DIR")
# Mount the shared Pi config directory as an additional workspace. Docker
# Sandboxes mount workspaces at their absolute host paths, matching the
# PI_CODING_AGENT_DIR configured in the kit.
#
# Set PI_SBX_AGENT_DIR= to disable this automatic mount, or point it at a
# different host-side Pi config directory if you also override the kit env var.
if [ -n "$PI_SBX_AGENT_DIR" ]; then
if [ ! -d "$PI_SBX_AGENT_DIR" ]; then
echo "sbx-pi: shared Pi config directory not found: $PI_SBX_AGENT_DIR" >&2
echo "Set PI_SBX_AGENT_DIR= to disable, or create the directory." >&2
exit 1
fi
args+=("$PI_SBX_AGENT_DIR")
fi
args+=("$@")
set_sandbox_env() {
sbx exec -d "$SANDBOX_NAME" bash -lc '
set -e
original_pwd=$1
file=/etc/sandbox-persistent.sh
tmp=$(mktemp)
if [ -f "$file" ]; then
grep -Ev "^export (PI_SBX_ORIGINAL_PWD|GIT_DISCOVERY_ACROSS_FILESYSTEM)=" "$file" > "$tmp" || true
fi
printf "export PI_SBX_ORIGINAL_PWD=%q\n" "$original_pwd" >> "$tmp"
printf "export GIT_DISCOVERY_ACROSS_FILESYSTEM=1\n" >> "$tmp"
cat "$tmp" > "$file"
rm -f "$tmp"
' bash "$ORIGINAL_PWD"
}
# If the sandbox already exists, run it by name. Recent sbx versions reject
# passing workspace arguments to an existing sandbox.
if sbx ls --quiet | grep -qxF "$SANDBOX_NAME"; then
set_sandbox_env
existing_args=("$SANDBOX_NAME" --kit "$KIT")
existing_args+=("$@")
exec sbx run "${existing_args[@]}"
fi
create_args=("${args[@]}" --name "$SANDBOX_NAME")
sbx create "${create_args[@]}"
set_sandbox_env
exec sbx run "$SANDBOX_NAME" --kit "$KIT" "$@"
Final thoughts
I had to iterate a lot in order to land on this configuration of sandboxes and I haven’t yet used them in very complex projects. The good news is that with an agent like Pi it’s really simple to state your vision and let it build the necessary scaffolding to make your life easier. The project is still in Early Access so it’s likely that it will keep getting improved to a point where a lot of this is not necessary.
As for Pi, it’s really as great as they say! You can use a Codex subscription to get frontier model tokens at a discount, but you get to use your custom-built harness. That might not sound like much. After all, won’t the Claude Code or Codex team be much better at building a harness than yourself? They might be better at creating a general-purpose one, but if you don’t care about MCPs, subagents, remote access, plan-mode, you’re paying a context penalty on all your tasks for features you never use. 3
Footnotes
-
In the meantime I’ve come to realize that even agent harnesses such as Claude Code or Codex only give you the impression of security with their permission systems. You can attempt to block destructive commands such as
rm -rf /but if you allow Bash or Python scripting those can be bypassed. One can argue that it’s on you to review the scripts before allowing them to run, but I think that defeats the purpose of using coding agents. ↩ -
The security model of the sandboxes is a good read. A sufficiently motivated agent will find a way of breaking out of a simple Docker container but that is impossible with a microVM. ↩
-
On that topic, I really recommend this quite short talk called A love letter to Pi by Lucas Meijer . ↩