Sandboxed Pi
#sandboxed-pi
A Docker container sandbox extension for pi. All tool execution (bash, read, write, edit, ls, grep, find) runs inside an ephemeral Docker container — nothing escapes to the host.
#Philosophy
Containment, not just isolation. Other approaches limit what tools can do (allow/deny lists, OS-level sandboxing). sandboxed-pi takes a harder line: every filesystem operation, every shell command, every search happens inside a container that is created on session start and destroyed on session end. The host filesystem is made available via a read-write mount of the current working directory (so your code is editable), but the container has no inherent access beyond that mount.
Fail-closed. If the container is unavailable, tools throw errors rather than silently falling back to host execution. The only way to run on the host is to opt out explicitly with --no-sandbox.
Transparent to the LLM. The extension overrides all seven built-in tools using pi's pluggable operations interfaces. The LLM sees the same tool interface — only the execution layer is replaced.
Non-root by default. Containers run as the host user (not root), so files created inside the container are owned by the host user and $USER / whoami work correctly.
#Features
- Full tool containment — bash, read, write, edit, ls, grep, find all execute via
docker exec - Ephemeral lifecycle — created on
session_start, destroyed onsession_shutdown - Non-root execution — containers run as the host user with matching UID/GID
- Custom user image — builds
pi-sandbox-<user>:<uid>per host user, cached across sessions - Container restart — if the container crashes mid-session, tools attempt a
docker startbefore failing !/!!user commands — also routed through the container- Act runner image — default is
ghcr.io/catthehacker/ubuntu:act-latest(git, node 24, python 3, curl, jq, make, gcc, docker CLI pre-installed) - Customizable — edit
Dockerfile.templateto add packages or tools - Opt-out — disable with
--no-sandbox
#Quick Start
#Installation
git clone https://github.com/rnorth/sandboxed-pi ~/.pi/agent/extensions/sandboxed-pi
The extension is auto-discovered by pi from ~/.pi/agent/extensions/sandboxed-pi/.
#Usage
# Start pi — container is created automatically pi # Custom image pi --sandbox-image ubuntu:24.04 # Disable sandboxing (run on the host) pi --no-sandbox # Inside pi (interactive mode), check container status /sandbox-status
Inside the container, the user identity matches the host:
whoami # → your username (not root) echo $HOME # → /home/pi echo $USER # → your username
#Customizing the container
Edit Dockerfile.template to add packages or tools. The template uses build arguments for the user configuration:
FROM ghcr.io/catthehacker/ubuntu:act-latest ARG USER_NAME ARG USER_UID ARG USER_GID # Add your customizations here RUN apt-get update && apt-get install -y my-favorite-tool # User is created automatically USER ${USER_NAME} CMD ["sleep", "infinity"]
The image is rebuilt on first container creation, so changes take effect on the next session. If a previously-cached image needs to be replaced, remove it manually with docker rmi pi-sandbox-<user>:<uid>.
#Configuration
| Flag | Type | Default | Description |
|---|---|---|---|
--sandbox-image |
string | ghcr.io/catthehacker/ubuntu:act-latest |
Base image used to build the per-user sandbox image |
--no-sandbox |
boolean | false |
Disable containerization; tools run on the host |
--egress-policy <file> |
string | "" |
Path to a policy file for egress control (mitmproxy sidecar). See egress control for details. |
--proxy-image |
string | "" |
Use a pre-built local proxy image instead of pulling from ghcr.io. For development. |
#Egress control
When --egress-policy is set, a mitmproxy sidecar container filters all outbound HTTP/HTTPS traffic from the sandbox. Each request is evaluated against a per-host list of ALLOW/DENY rules (last match wins); unmatched requests and unlisted hosts return 403 Access denied by egress policy.
This is non-voluntary — it uses iptables REDIRECT inside a shared network namespace, so it intercepts traffic from tools that ignore HTTP_PROXY (Go binaries, statically compiled tools, anything that opens raw sockets).
#Policy file format
networkPolicies: - host: api.github.com policies: - action: DENY path: /.* method: "*" - action: ALLOW path: /repos/.* method: GET - action: ALLOW path: /users/.* method: GET - host: registry.npmjs.org policies: - action: ALLOW path: /.* method: "*"
Schema:
| Field | Type | Description |
|---|---|---|
networkPolicies |
NetworkPolicy[] |
Top-level array |
host |
string |
Exact hostname to match |
policies |
Rule[] |
Ordered list of allow/deny rules |
action |
"ALLOW" | "DENY" |
Rule action |
path |
string |
Python regex (re module, fullmatch) — matched against the path only, query string excluded |
method |
string |
HTTP method (GET, POST, * for all) |
Matching semantics:
- Rules are evaluated top-to-bottom (in declaration order)
- The last matching rule wins (like iptables)
- If no rule matches, the request is DENIED (default-deny)
See examples/github-read-only.yaml for a working example.
#How it works
pi --egress-policy policy.yaml
├── proxy container starts (NET_ADMIN, mitmproxy + iptables in entrypoint)
├── workload container starts with --network container:<proxy> (shared netns)
└── iptables REDIRECT inside that netns sends TCP 80/443 to mitmproxy:8080
(--uid-owner exempts the proxy's own upstream traffic)
tool call → docker exec workload <cmd>
→ kernel redirects sockets to mitmproxy (transparent, ignores HTTP_PROXY)
→ policy evaluation (ALLOW/DENY rules; default-deny → 403)
→ TLS-terminate, re-encrypt to upstream
→ audit log written to /var/log/sandboxed-pi/audit.log
The audit log is tailed and printed to stderr (and visible in the pi UI as info notifications) as requests are processed.
#Limitations
- Only HTTP/HTTPS traffic is policy-filtered. All other outbound traffic (non-standard TCP ports, SSH, raw UDP, IPv6) is blocked at the firewall — not passed through unfiltered.
- DNS is intercepted: only policy-listed hostnames can be resolved. All other queries return NXDOMAIN. Resolved IP↔hostname bindings are used to verify each connection's destination IP, blocking direct-to-IP connections with spoofed Host/SNI.
- WebSocket connections are only policy-checked at the initial HTTP upgrade request. Frames sent after the upgrade are not inspected — a workload can use an allowed WebSocket endpoint as an arbitrary data channel.
- Cert-pinned clients fail against the mitmproxy CA.
#How it works
For runtime mechanics and design rationale, see the feature docs:
- Container sandbox — containment model, session lifecycle, tool-call flow, container resilience, fail-closed
- Container configuration — path mounting, non-root execution
- Egress control — mitmproxy sidecar, policy format, limitations
For a high-level orientation, see docs/architecture.md.
#Development
#Prerequisites
- Docker
- Node.js 22+
- pi
#Setup
npm install
#Test
npm test
Integration tests require a running Docker daemon. Unit tests mock Docker operations.
#Project structure
sandboxed-pi/ ├── pi-extension/ │ ├── src/ │ │ ├── index.ts # Extension entry point (lifecycle, tool overrides, flags) │ │ ├── docker.ts # Low-level Docker helpers (container lifecycle, exec, image build) │ │ ├── ops.ts # Operations factories for all 7 built-in tools │ │ └── egress.ts # Egress proxy lifecycle, policy parsing, audit log tailing │ ├── tests/ │ │ ├── ops.test.ts # Unit tests for operation factories │ │ ├── egress.test.ts # Unit tests for policy parsing and validation │ │ └── docker.integration.test.ts # Integration tests for Docker helpers │ ├── Dockerfile.template # Template for the per-user sandbox image │ ├── tsconfig.json │ └── vitest.config.ts ├── proxy/ │ ├── Dockerfile # Image for the mitmproxy egress sidecar │ ├── entrypoint.py # Proxy entrypoint: iptables setup + mitmproxy launch │ ├── policy.py # mitmproxy addon: policy evaluation + audit log │ ├── dns_interceptor.py # DNS interception addon │ ├── setup_iptables.py # iptables/ip6tables REDIRECT rule setup │ └── tests/ ├── docs/ │ ├── architecture.md # High-level orientation and links to feature docs │ ├── container-sandbox.md # Containment model, lifecycle, tool-call flow │ ├── container-configuration.md # Path mounting, non-root execution │ └── egress-control.md # mitmproxy sidecar, policy format, limitations ├── scripts/ ├── examples/ └── package.json