Skip to content

Agent Sandbox

PizzaPi can enforce OS-level sandboxing around every tool call the agent makes — bash commands, file reads/writes, and MCP server interactions. This prevents prompt injections or model hallucinations from accessing sensitive files, exfiltrating data, or damaging your system.

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.

ModeFilesystemNetworkSummary
noneUnrestrictedUnrestrictedSandbox disabled. All tools run directly on the host.
basicProtected ✅UnrestrictedBlocks reads/writes of sensitive dotfiles. Network is unrestricted. (default)
fullProtected ✅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

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:

CategoryProtected paths
Read blocked~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud, ~/.docker/config.json, browser data dirs
Write blocked.env, .env.local, ~/.ssh
Write allowedCurrent working directory (.), /tmp

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

Any field you add under sandbox is a srt-native override merged on top of the preset. The format matches ~/.srt-settings.json exactly.

{
"sandbox": {
"mode": "basic",
"filesystem": {
"denyRead": ["~/.kube", "~/.terraform"],
"allowWrite": [".", "/tmp", "~/my-project"],
"denyWrite": [".env.production"]
}
}
}
  • denyRead — additional paths to block from reading (merged with preset)
  • allowWrite — paths the agent may write to (replaces preset when specified)
  • denyWrite — additional paths to block from writing (merged with preset)
{
"sandbox": {
"mode": "full",
"network": {
"allowedDomains": ["*.github.com", "api.example.com"],
"deniedDomains": ["ads.example.com"],
"allowLocalBinding": true,
"allowUnixSockets": ["/var/run/docker.sock"]
}
}
}
  • allowedDomains — domains the agent can reach (allow-only; [] = deny all)
  • deniedDomains — domains to explicitly block
  • allowLocalBinding — allow the process to bind local ports (default: true)
  • allowUnixSockets — Unix socket paths to allow (blocked by default)
  • allowAllUnixSockets — disable Unix socket blocking entirely

These pass through directly to sandbox-runtime:

{
"sandbox": {
"mode": "basic",
"mandatoryDenySearchDepth": 5,
"enableWeakerNetworkIsolation": true,
"allowPty": true,
"ignoreViolations": {
"jest": ["/tmp"]
}
}
}

See the srt documentation for details on each option.


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:

FieldMerge behavior
modeProject can only escalate (none → basic → full), never weaken
filesystem.denyReadUnion — project adds more denials, never removes global ones
filesystem.denyWriteUnion — same
filesystem.allowWriteIntersection — project can only narrow, never widen
network.allowedDomainsIntersection — project can only narrow
network.deniedDomainsUnion — project adds more denials

PlatformEnforcementStatus
macOSsandbox-exec (built-in)✅ Fully supported
Linuxbubblewrap + seccomp✅ Supported — requires bubblewrap and socat
Windows⚠️ Graceful degradation (sandbox disabled)

On Linux, install dependencies:

Terminal window
sudo apt install bubblewrap socat

On unsupported platforms, the sandbox initializes but is inactive — all tools execute without restrictions.


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). In sessions connected to the PizzaPi relay, violations are forwarded as sandbox:violation events visible in the web UI.

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:

{
"mode": "basic",
"active": true,
"platform": "darwin",
"violations": 3,
"recentViolations": [
{
"timestamp": "2026-03-29T14:22:01.000Z",
"operation": "read",
"target": "~/.ssh/id_ed25519",
"reason": "Path in denyRead"
}
],
"config": { "mode": "basic", "filesystem": { "..." : "..." } }
}
FieldDescription
modeActive sandbox mode (none, basic, full)
activeWhether enforcement is actually running (may be false on unsupported platforms)
platformRunner’s OS (darwin, linux, win32)
violationsTotal violation count since runner start
recentViolationsLast 20 violations (newest first)
configFully resolved sandbox config (global + project merged)

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:

{
"saved": true,
"resolvedConfig": { "..." : "..." },
"message": "Changes will apply on next session start."
}
  • Send null for a key to remove it from the config (e.g., clearing stale network rules when downgrading from full to basic).
  • Nested objects are shallow-merged; arrays and scalars are replaced.
  • Validation rejects invalid modes and non-string array entries with a 400 status.

When a runner is connected to the PizzaPi relay, every sandbox:violation event is forwarded in real time. In the web UI:

  • Violations appear as warning badges in the session timeline.
  • The runner detail panel shows a live violation counter and a filterable violation log.
  • Clicking a violation expands it to show the full operation, target path, and denial reason.

The /sandbox command is available inside any active session:

CommandDescription
/sandboxStatus overview — mode, active state, platform, violation count
/sandbox violationsFull violation log (last 100 entries)
/sandbox configShow 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):

VariableNotes
PIZZAPI_WORKSPACE_ROOTSPreferred — comma-separated list
PIZZAPI_WORKSPACE_ROOTConvenience alias for a single root
PIZZAPI_RUNNER_ROOTSLegacy 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.
  • 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:

MechanismScopeEnforces
Workspace rootsRunner level — controls which directories the daemon will start sessions inWhere sessions can be created
Sandbox filesystemSession level — controls what the agent can read/write during executionWhat 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”

Section titled “"Sandbox init failed, continuing unsandboxed””

The sandbox runtime failed to initialize. The session continues without protection. 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:

FlagWhat it skipsWhen to use
--safe-modeEverything belowFastest possible startup
--no-mcpMCP server connectionsMCP server is down or slow
--no-pluginsClaude Code pluginsPlugin scanning is slow
--no-hooksHook executionA hook script is hanging
--no-relayRelay server connectionRelay 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:

VariableEffect
PIZZAPI_NO_MCP=1Skip MCP servers
PIZZAPI_NO_PLUGINS=1Skip plugin loading
PIZZAPI_NO_HOOKS=1Skip hook execution
PIZZAPI_NO_RELAY=1Skip 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.

Suppress this warning if you expect slow startup:

~/.pizzapi/config.json
{
"slowStartupWarning": false
}
KeyTypeDefaultDescription
mcpTimeoutnumber30000Per-server timeout (ms) for MCP tools/list calls. 0 to disable.
slowStartupWarningbooleantrueShow warnings when startup exceeds 5 seconds.