Skip to content

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.


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:

Terminal window
pizza --sandbox full
pizza --sandbox basic
pizza --sandbox none

pizza --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:

Terminal window
PIZZAPI_SANDBOX=full pizza
PIZZAPI_NO_SANDBOX=1 pizza # shorthand for none

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.


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 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.


All keys under sandbox other than mode are overrides passed through to the resolved sandbox-runtime config when mode is not none.

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.
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.

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.


{
"sandbox": {
"mode": "full",
"network": {
"allowedDomains": ["*.github.com", "api.anthropic.com", "registry.npmjs.org"]
}
}
}
{
"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"]
}
}
}

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 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:

Terminal window
sudo apt install bubblewrap socat

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.


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 config

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 config path, sending null for a key removes it from the config (e.g., clearing stale network rules when downgrading from full to basic).
  • On the REST PUT /api/runners/{runnerId}/sandbox-config endpoint, null is currently written as a JSON null value 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 400 status.

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.

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)

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.

Set a comma-separated list of allowed directories:

Terminal window
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.

  • Paths are normalized — trailing slashes are stripped, symlinks are resolved.
  • A session’s cwd must be equal to or a subdirectory of at least one root.
  • Path traversal attacks (/../) are neutralized via realpathSync before 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 and mkdir -p would, including after a not-yet-existing component (missing/../link/wt is judged by where link points). 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).

To persist workspace roots for the runner daemon, add them to the LaunchAgent plist:

~/Library/LaunchAgents/com.pizzapi.runner.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:

Terminal window
launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plist
launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist

Relationship 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.


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": []
}
}
}

The agent tried to write outside allowWrite. Add the path:

{
"sandbox": {
"filesystem": {
"allowWrite": [".", "/tmp", "/custom/output"]
}
}
}

Add the domain to allowedDomains:

{
"sandbox": {
"mode": "full",
"network": {
"allowedDomains": ["*.github.com", "api.example.com"]
}
}
}
Terminal window
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: bubblewrap not installed
  • macOS: permission issue with sandbox-exec
  • Incompatible OS version

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.

If pizza is hanging or taking too long:

Terminal window
# Skip everything external — instant startup
pizza --safe-mode

This disables MCP servers, plugins, hooks, and the relay connection. You get a fully functional coding session without any of the integrations.

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:

Terminal window
# Skip only MCP servers (keep relay, plugins, hooks)
pizza --no-mcp
# Skip MCP and plugins but keep relay
pizza --no-mcp --no-plugins
# Equivalent to PIZZAPI_RELAY_URL=off
pizza --no-relay

Environment 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

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.

When MCP initialization takes longer than 5 seconds, PizzaPi shows a warning with a per-server timing breakdown:

⏱ MCP startup took 12.3s
Slow 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:

~/.pizzapi/config.json
{
"slowStartupWarning": false
}
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.