Agent Sandbox
PizzaPi can enforce OS-level sandboxing around the agent worker’s shell and file-tool processes. The policy limits filesystem access and, in full mode, outbound network access. It does not constrain browser sessions, standalone service panels, or necessarily every external MCP server’s own effects; treat connected integrations as separately trusted capabilities. Sandboxing reduces risk but is not a security boundary for untrusted workloads.
The sandbox is powered by Anthropic’s sandbox-runtime, which uses macOS sandbox-exec and Linux bubblewrap + seccomp to enforce restrictions at the kernel level.
Sandbox Modes
Section titled “Sandbox Modes”PizzaPi provides three preset modes. The default is basic.
| Mode | Filesystem | Network | Summary |
|---|---|---|---|
none |
Unrestricted | Unrestricted | Sandbox disabled. All tools run directly on the host. |
basic |
Protected ✅ | Unrestricted | Blocks reads/writes of sensitive dotfiles. Network is unrestricted. (default) |
full |
Protected ✅ | Deny-all by default ✅ | Full enforcement. Network is blocked unless you explicitly allow domains. |
Configure the mode in ~/.pizzapi/config.json:
{ "sandbox": { "mode": "full" }}Or via CLI flag:
pizza --sandbox fullpizza --sandbox basicpizza --sandbox nonepizza --help advertises the aliases enforce, audit, and off, which map to full, basic, and none. The aliases are accepted by the CLI flag and PIZZAPI_SANDBOX; in JSON config, use canonical none, basic, or full.
Or environment variable:
PIZZAPI_SANDBOX=full pizzaPIZZAPI_NO_SANDBOX=1 pizza # shorthand for noneWhat basic protects
Section titled “What basic protects”The basic preset blocks the most common exfiltration targets:
| Category | Protected paths |
|---|---|
| Read blocked | ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud, ~/.docker/config.json, browser data dirs |
| Write blocked | .env, .env.local, ~/.ssh |
| Write allowed | Current working directory (.), OS temp directory (os.tmpdir()) |
Network access is unrestricted in basic mode — the agent can connect to any host.
What full adds
Section titled “What full adds”The full preset adds network enforcement on top of basic:
- Network deny-all by default — all outbound connections are blocked unless you add
allowedDomains. - Same filesystem protections as
basic.
To open specific domains in full mode, add overrides:
{ "sandbox": { "mode": "full", "network": { "allowedDomains": [ "*.github.com", "api.anthropic.com", "registry.npmjs.org" ] } }}Preset defaults
Section titled “Preset defaults”| Preset | Default value |
|---|---|
mode |
basic |
basic.filesystem.denyRead |
~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud, ~/.docker/config.json, Chrome/Firefox profile paths on macOS, Linux, and Windows |
basic.filesystem.allowWrite |
., OS temp directory (os.tmpdir()) |
basic.filesystem.denyWrite |
.env, .env.local, ~/.ssh |
basic.network |
No network key is emitted by config resolution; the tools layer supplies permissive runtime defaults. |
full.network.allowedDomains |
[] (deny all outbound domains) |
full.network.deniedDomains |
[] |
full.network.allowLocalBinding |
true |
full.filesystem.* |
Same as basic |
Relative filesystem paths are resolved against the session cwd; ~ expands to the user’s home directory.
Config reference
Section titled “Config reference”All keys under sandbox other than mode are overrides passed through to the resolved sandbox-runtime config when mode is not none.
Top-level keys
Section titled “Top-level keys”| Key | Type | Default | Meaning |
|---|---|---|---|
mode |
"none" | "basic" | "full" |
"basic" |
Selects the preset. Unknown modes fail startup config resolution instead of silently weakening protection. |
filesystem |
object | preset filesystem config | Filesystem read/write policy. |
network |
object | full: deny-all; basic: unrestricted unless allowedDomains is set |
Network and Unix-socket policy. |
ignoreViolations |
Record<string, string[]> |
unset | Passed through to sandbox-runtime. In global+project merging, only the global value is used. |
enableWeakerNetworkIsolation |
boolean |
unset / false behavior | Passed through to sandbox-runtime. Project config cannot turn this on unless global config already did. |
enableWeakerNestedSandbox |
boolean |
unset / false behavior | Allows nested sandbox execution with a weaker profile. Project config cannot turn this on unless global config already did. |
mandatoryDenySearchDepth |
number |
sandbox-runtime default; PizzaPi comments document 3 |
Linux mandatory-deny path scanning depth. In global+project merging, only the global value is used. |
allowPty |
boolean |
unset / false behavior | Allows pseudo-terminal allocation inside the sandbox. Project config cannot turn this on unless global config already did. |
filesystem
Section titled “filesystem”| Key | Type | Default | Meaning |
|---|---|---|---|
denyRead |
string[] |
sensitive-path preset | Additional paths the agent cannot read. User values are unioned with the preset. |
allowWrite |
string[] |
[".", os.tmpdir()] |
Paths the agent may write to. In a single config, specifying this replaces the preset allow list. |
denyWrite |
string[] |
.env, .env.local, ~/.ssh |
Additional paths the agent cannot write. Deny rules take precedence over allowWrite. |
allowGitConfig |
boolean |
unset / false behavior | Passed to sandbox-runtime to allow writes to .gitconfig. Project config cannot turn this on unless global config already did. |
PizzaPi also performs pre-call path validation for file tools. Reads fail if the normalized path is under denyRead. Writes fail if the path is under denyWrite or not under any allowWrite entry.
network
Section titled “network”| Key | Type | Default | Meaning |
|---|---|---|---|
allowedDomains |
string[] |
full: []; basic: unset |
Domains allowed by the network sandbox. In full, [] means deny all. In basic, setting this key opts into network sandboxing. |
deniedDomains |
string[] |
[] |
Domains explicitly denied. |
allowLocalBinding |
boolean |
true when PizzaPi creates a network config |
Allows the process to bind local ports. Project config cannot turn this on unless global config already did. |
allowUnixSockets |
string[] |
unset | Unix socket paths allowed by sandbox-runtime. PizzaPi also appends $SSH_AUTH_SOCK when detected. |
allowAllUnixSockets |
boolean |
basic permissive runtime fallback: true; otherwise unset |
Allows all Unix sockets. On Linux this disables sandbox-runtime’s AF_UNIX blocking. Project config cannot turn this on unless global config already did. |
httpProxyPort |
number |
sandbox-runtime default | HTTP proxy port override. |
socksProxyPort |
number |
sandbox-runtime default | SOCKS proxy port override. |
enableWeakerNetworkIsolation |
— | — | Not a network child key in PizzaPi config; use top-level sandbox.enableWeakerNetworkIsolation. |
When the network proxy is active, PizzaPi exposes proxy environment variables to sandboxed child processes: HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy, ALL_PROXY, and all_proxy as applicable.
Example policies
Section titled “Example policies”Full mode with selected network access
Section titled “Full mode with selected network access”{ "sandbox": { "mode": "full", "network": { "allowedDomains": ["*.github.com", "api.anthropic.com", "registry.npmjs.org"] } }}Basic mode with narrower write access
Section titled “Basic mode with narrower write access”{ "sandbox": { "mode": "basic", "filesystem": { "allowWrite": [".", "/tmp/pizzapi-output"], "denyRead": ["~/.kube", "~/.terraform"], "denyWrite": [".env.production"] } }}Basic mode with network sandboxing enabled
Section titled “Basic mode with network sandboxing enabled”{ "sandbox": { "mode": "basic", "network": { "allowedDomains": ["api.example.com"], "deniedDomains": ["telemetry.example.com"] } }}SSH Agent Detection
Section titled “SSH Agent Detection”The sandbox automatically detects your SSH agent socket via $SSH_AUTH_SOCK and adds it to the Unix socket allowlist. This means git push, git pull, and other SSH operations work transparently inside the sandbox.
Merge Semantics (project vs global config)
Section titled “Merge Semantics (project vs global config)”When both global (~/.pizzapi/config.json) and project (.pizzapi/config.json) configs exist:
| Field | Merge behavior |
|---|---|
mode |
Project can only escalate (none → basic → full), never weaken |
filesystem.denyRead |
Union — project adds more denials, never removes global ones |
filesystem.denyWrite |
Union — same |
filesystem.allowWrite |
Intersection when both configs specify it; if global omits it, the project value is ignored so the project cannot widen the preset |
filesystem.allowGitConfig |
Global wins; if global omits it, project can only set false |
network.allowedDomains |
Intersection when both configs specify it; if global omits it, the project value is ignored so the project cannot widen access |
network.deniedDomains |
Union — project adds more denials |
network.allowUnixSockets |
Intersection when both configs specify it |
network.allowAllUnixSockets, network.allowLocalBinding |
Global wins; if global omits it, project can only set false |
network.httpProxyPort, network.socksProxyPort |
Global wins when set |
ignoreViolations, mandatoryDenySearchDepth |
Global-only |
enableWeakerNetworkIsolation, enableWeakerNestedSandbox, allowPty |
Global wins; if global omits them, project can only set false |
Platform Support
Section titled “Platform Support”| Platform | Enforcement | Status |
|---|---|---|
| macOS | sandbox-exec (built-in) |
✅ Fully supported |
| Linux | bubblewrap + seccomp |
✅ Supported — requires bubblewrap and socat |
| Windows | — | ⚠️ Not supported. The implicit default degrades; an explicitly requested mode refuses to start (see below) |
On Linux, install dependencies:
sudo apt install bubblewrap socatWhen the sandbox cannot be enabled
Section titled “When the sandbox cannot be enabled”What happens on an unsupported platform, or when sandbox initialization fails (for example, bubblewrap is missing), depends on how the mode was chosen:
| How the mode was chosen | Behavior when the sandbox cannot be enabled |
|---|---|
Explicitly: --sandbox or PIZZAPI_SANDBOX set to basic/full, or sandbox.mode set to basic/full in ~/.pizzapi/config.json (including through the web UI’s sandbox settings) |
Fails closed. The session refuses to start and reports why. If startup somehow continues, the bash tool refuses every command. |
Implicitly (no global sandbox.mode and no override, so the basic default applies) |
Logs a warning and continues unsandboxed. /sandbox shows Active: no. |
A project-local .pizzapi/config.json can escalate the mode, but it cannot make startup fail closed by itself.
For runner sessions, the runner reports a new session as ready only after its worker confirms that the sandbox stage passed. When a fail-closed worker refuses to start, the runner reports a spawn error with the worker’s reason instead, so the web UI and the spawn API show the failure (the spawn request fails if this happens within its acknowledgement window) rather than a session that disappears. If a worker has not reported within 20 seconds, the runner falls back to reporting it ready; the sandbox is still enforced, because the worker exits before running any tool.
To accept unsandboxed execution for an explicitly requested mode, for example on Windows, set PIZZAPI_SANDBOX_ALLOW_UNSANDBOXED=1. You can also choose none explicitly.
Violation Logging
Section titled “Violation Logging”When the sandbox blocks an operation, it records a violation event:
- Timestamp
- Operation (
read/write) - Target path
- Reason for denial
Violations are stored in a ring buffer (100 entries max) inside the worker process. They are not streamed to the relay or web UI in real time — they are visible only through the /sandbox slash command in an active session.
Use the /sandbox slash command in a session to view status and recent violations:
/sandbox # status overview/sandbox violations # full violation log/sandbox config # show resolved configRuntime Inspection & Configuration API
Section titled “Runtime Inspection & Configuration API”PizzaPi exposes REST endpoints and a slash command to inspect and update sandbox configuration at runtime — no restart required.
GET /api/runners/{runnerId}/sandbox-status
Section titled “GET /api/runners/{runnerId}/sandbox-status”Returns the current sandbox state for a runner. Requires an authenticated session owned by the runner’s user.
Response:
{ "ok": true, "mode": "basic", "active": true, "configured": true, "platform": "darwin", "violations": 0, "recentViolations": [], "config": { "mode": "basic", "filesystem": { "..." : "..." } }, "rawConfig": { "mode": "basic" }}| Field | Description |
|---|---|
ok |
true if the status query succeeded |
mode |
Active sandbox mode (none, basic, full) |
active |
true when a non-none mode is configured; this does not guarantee enforcement is currently running on the worker |
configured |
Same as active — a non-none mode is configured |
platform |
Runner’s OS (darwin, linux, win32) |
violations |
Always 0 from this REST endpoint (the daemon does not run inside a sandbox) |
recentViolations |
Always empty from this REST endpoint |
config |
Fully resolved global sandbox config (project-local overrides are excluded) |
rawConfig |
The raw global sandbox config as stored in ~/.pizzapi/config.json |
Real-time violations and the fully resolved per-session config are only available via the in-session /sandbox slash command.
PUT /api/runners/{runnerId}/sandbox-config
Section titled “PUT /api/runners/{runnerId}/sandbox-config”Update sandbox configuration at runtime without restarting the runner. The body is deep-merged with the existing global config in ~/.pizzapi/config.json.
Request body:
{ "mode": "full", "network": { "allowedDomains": ["*.github.com", "api.anthropic.com"] }}Response:
{ "ok": true, "saved": true, "resolvedConfig": { "..." : "..." }, "message": "Changes will apply on next session start."}- On the in-session
/sandbox configpath, sendingnullfor a key removes it from the config (e.g., clearing stale network rules when downgrading fromfulltobasic). - On the REST
PUT /api/runners/{runnerId}/sandbox-configendpoint,nullis currently written as a JSONnullvalue rather than deleting the key. The effective sandbox value is still cleared, but the key remains in~/.pizzapi/config.json. - Nested objects are shallow-merged; arrays and scalars are replaced.
- Validation rejects invalid modes and non-string array entries with a
400status.
Violation events in the web UI
Section titled “Violation events in the web UI”Real-time sandbox:violation forwarding to the web UI is not currently implemented. Violations are logged in the worker and surfaced through the /sandbox slash command in an active session.
/sandbox slash command
Section titled “/sandbox slash command”The /sandbox command is available inside any active session:
| Command | Description |
|---|---|
/sandbox |
Status overview — mode, active state, platform, violation count |
/sandbox violations |
Full violation log (last 100 entries) |
/sandbox config |
Show the fully resolved config (global + project merged) |
Runner Workspace Roots
Section titled “Runner Workspace Roots”The PIZZAPI_WORKSPACE_ROOTS environment variable restricts which working directories the runner daemon will accept when spawning new sessions. Any spawn request whose cwd is outside the allowed roots is rejected before a session starts.
This is useful for shared or team runners that should only work within specific project directories — e.g., a CI runner locked to /srv/projects or a team Mac Mini restricted to a few repos.
Setting workspace roots
Section titled “Setting workspace roots”Set a comma-separated list of allowed directories:
export PIZZAPI_WORKSPACE_ROOTS="/Users/jordan/Projects,/tmp/scratch"Aliases (checked in priority order):
| Variable | Notes |
|---|---|
PIZZAPI_WORKSPACE_ROOTS |
Preferred — comma-separated list |
PIZZAPI_WORKSPACE_ROOT |
Convenience alias for a single root |
PIZZAPI_RUNNER_ROOTS |
Legacy alias (back-compat) |
If PIZZAPI_WORKSPACE_ROOTS is set, the other variables are ignored. If none are set, the runner is unscoped and accepts any cwd.
Behavior
Section titled “Behavior”- Paths are normalized — trailing slashes are stripped, symlinks are resolved.
- A session’s
cwdmust be equal to or a subdirectory of at least one root. - Path traversal attacks (
/../) are neutralized viarealpathSyncbefore comparison. - Paths that do not exist yet (for example a new Git worktree destination) are resolved through their nearest existing ancestor, so an in-root symlink that points outside the roots cannot be used as a parent.
..segments are applied after following symlinks, as Git andmkdir -pwould, including after a not-yet-existing component (missing/../link/wtis judged by wherelinkpoints). A path that passes through a dangling or unresolvable symlink is rejected. - On Windows, comparisons are case-insensitive.
- The filesystem root (
/) as a root allows everything (equivalent to unscoped).
LaunchAgent integration
Section titled “LaunchAgent integration”To persist workspace roots for the runner daemon, add them to the LaunchAgent plist:
<key>EnvironmentVariables</key><dict> <key>PIZZAPI_WORKSPACE_ROOTS</key> <string>/Users/jordan/Projects,/Users/jordan/Clients</string></dict>After editing the plist, reload the LaunchAgent:
launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plistlaunchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plistRelationship to sandbox filesystem restrictions
Section titled “Relationship to sandbox filesystem restrictions”Workspace roots and sandbox restrictions are complementary:
| Mechanism | Scope | Enforces |
|---|---|---|
| Workspace roots | Runner level — controls which directories the daemon will start sessions in | Where sessions can be created |
| Sandbox filesystem | Session level — controls what the agent can read/write during execution | What the agent can access at runtime |
A session must pass both checks: the runner must allow the cwd (workspace roots), and then the sandbox must allow each individual file operation (filesystem rules). Neither replaces the other.
Troubleshooting
Section titled “Troubleshooting”“Sandbox blocked: Read denied”
Section titled ““Sandbox blocked: Read denied””The agent tried to read a file in denyRead. To allow it, remove the path from your config’s denyRead, or narrow the deny rule:
{ "sandbox": { "filesystem": { "denyRead": [] } }}“Sandbox blocked: Write denied”
Section titled ““Sandbox blocked: Write denied””The agent tried to write outside allowWrite. Add the path:
{ "sandbox": { "filesystem": { "allowWrite": [".", "/tmp", "/custom/output"] } }}Network blocked in full mode
Section titled “Network blocked in full mode”Add the domain to allowedDomains:
{ "sandbox": { "mode": "full", "network": { "allowedDomains": ["*.github.com", "api.example.com"] } }}Linux: “bubblewrap not found”
Section titled “Linux: “bubblewrap not found””sudo apt install bubblewrap socat“Sandbox init failed, continuing unsandboxed” / “Refusing to start”
Section titled ““Sandbox init failed, continuing unsandboxed” / “Refusing to start””The sandbox runtime failed to initialize. If you did not choose a mode explicitly, the session continues without protection. If you did, the session refuses to start; see When the sandbox cannot be enabled. Check the error message — common causes:
- Linux:
bubblewrapnot installed - macOS: permission issue with
sandbox-exec - Incompatible OS version
Safe Mode & Startup Diagnostics
Section titled “Safe Mode & Startup Diagnostics”PizzaPi connects to external services during startup — MCP servers, the relay server, Claude Code plugins, and hooks. If any of these are slow or unreachable, your CLI session can take a long time to become interactive.
Safe mode lets you skip some or all of these dependencies for an instant startup.
Quick Fix: Safe Mode
Section titled “Quick Fix: Safe Mode”If pizza is hanging or taking too long:
# Skip everything external — instant startuppizza --safe-modeThis disables MCP servers, plugins, hooks, and the relay connection. You get a fully functional coding session without any of the integrations.
Granular Skip Flags
Section titled “Granular Skip Flags”If you only want to skip specific subsystems:
| Flag | What it skips | When to use |
|---|---|---|
--safe-mode |
Everything below | Fastest possible startup |
--no-mcp |
MCP server connections | MCP server is down or slow |
--no-plugins |
Claude Code plugins | Plugin scanning is slow |
--no-hooks |
Hook execution | A hook script is hanging |
--no-relay |
Relay server connection | Relay is unreachable |
Examples:
# Skip only MCP servers (keep relay, plugins, hooks)pizza --no-mcp
# Skip MCP and plugins but keep relaypizza --no-mcp --no-plugins
# Equivalent to PIZZAPI_RELAY_URL=offpizza --no-relayEnvironment Variables for Runner / Worker Mode
Section titled “Environment Variables for Runner / Worker Mode”When running sessions through the runner daemon, you can pass safe-mode options via environment variables:
| Variable | Effect |
|---|---|
PIZZAPI_NO_MCP=1 |
Skip MCP servers |
PIZZAPI_NO_PLUGINS=1 |
Skip plugin loading |
PIZZAPI_NO_HOOKS=1 |
Skip hook execution |
PIZZAPI_NO_RELAY=1 |
Skip relay connection |
What Causes Slow Startup?
Section titled “What Causes Slow Startup?”MCP Servers Most common — Each configured MCP server must spawn, initialize, and respond to tools/list. Common culprits include npx commands that download packages on first run, Docker-based servers that pull images, and unreachable HTTP endpoints. PizzaPi initializes all servers in parallel and applies a per-server timeout (30s default).
// ~/.pizzapi/config.json — increase timeout for slow servers{ "mcpTimeout": 60000}Relay server — If unreachable, the initial handshake can take 10–30 seconds. Fix: --no-relay or "relayUrl": "off".
Plugins — Plugin discovery scans multiple directories. Usually fast but can be slow on network-mounted drives. Fix: --no-plugins.
Hooks — A hanging hook script blocks startup. Fix: --no-hooks or fix the script.
Slow-Startup Warnings
Section titled “Slow-Startup Warnings”When MCP initialization takes longer than 5 seconds, PizzaPi shows a warning with a per-server timing breakdown:
⏱ MCP startup took 12.3sSlow servers: • playwright: 8.2s • tavily: 4.1s (timed out)
Tip: Use --safe-mode or --no-mcp for instant startup.Disable this warning: set slowStartupWarning: false in config.Suppress this warning if you expect slow startup:
{ "slowStartupWarning": false}Startup Diagnostics Configuration
Section titled “Startup Diagnostics Configuration”| Key | Type | Default | Description |
|---|---|---|---|
mcpTimeout |
number |
30000 |
Per-server timeout (ms) for MCP tools/list calls. 0 to disable. |
slowStartupWarning |
boolean |
true |
Show warnings when startup exceeds 5 seconds. |