Skip to content

Sandbox

For Pi-native agents, the sandbox confines mono-agent-owned commands by wrapping them with srt (the native sandbox runtime) and a generated settings file: a filesystem scope (readable/writable roots, deny-write globs), a network policy, and a fallback for when the sandbox engine is unavailable. This includes both Bash commands and the child process behind NodeRepl. This page covers the sandbox config block and the monotonic merge that lets request-scoped policies tighten — but never widen — the configured baseline.

The whole block is config coverage backed by @mono-agent/turn/host.

{
"sandbox": {
"mode": "native",
"network": { "mode": "none", "allowlist": [] },
"readableRoots": ["."],
"writableRoots": ["."],
"denyWrite": [".env", ".env.*", ".git/config", ".git/hooks/**"],
"fallback": "fail-closed",
"unsafeAllowHostProcess": false
}
}
KeyType / valuesDefaultEnv var
sandbox.modenative (srt-wrapped) | offnative—
sandbox.network.modenone | localhost | allowlist | allnone—
sandbox.network.allowliststring[] of host suffixes (*.suffix wildcards)[]—
sandbox.readableRootsstring[] of paths["."] (workspace)—
sandbox.writableRootsstring[] of paths["."] (workspace)—
sandbox.denyWritestring[] of globssee defaults below—
sandbox.fallbackfail-closed | unsafe-host-processfail-closed—
sandbox.unsafeAllowHostProcessbooleanfalse—

The engine id is srt (the only built-in engine). On macOS, mono-agent can install and own the pinned runtime itself; a global srt is not required.

Manage and prove it with:

Terminal window
mono-agent sandbox status
mono-agent sandbox setup # macOS: private per-user cache, then full check
mono-agent sandbox check
mono-agent validate --preset code-sandbox

Managed setup installs exact @anthropic-ai/sandbox-runtime 0.0.64 dependencies from the checked-in lock into an owner-only version/hash directory under the user’s cache. It does not modify PATH, global npm packages, system packages, or another user’s files. Installation uses a private identity-bound lock, stages with scripts disabled, verifies the complete tree against an independently pinned digest, and atomically promotes it. The install marker records that trusted digest but cannot define it, so rewriting the tree and marker still fails closed. An unsafe/corrupt existing tree is quarantined and repaired only inside that managed cache.

The managed cache must be on a local filesystem with working BSD advisory file locks. Network-mounted cache overrides such as NFS or CIFS are unsupported for managed setup because their lock semantics can differ across clients.

v0.9.0 also began writing v2 directory install locks whose owner records carry process incarnation identity. The bounded, fail-closed reader for owner-only v1 files is intentionally retained indefinitely: an upgrade can skip releases and later encounter a file left by a crashed v0.8-or-earlier installer. New writes never use the v1 format. This permanent compatibility decision is recorded in the canonical deprecation tracker.

Selecting managed SRT during guided init always runs this idempotent managed setup and its functional postcondition, even when a compatible external srt already exists on PATH. External SRT remains a status/check and runtime compatibility path; it does not satisfy the guided managed-install choice.

status distinguishes managed, compatible external, absent, corrupt, and unsupported states. check proves real enforcement rather than trusting --version: allowed workspace read/write, sibling-secret read denial, .env and out-of-root write denial, localhost access, and denial of a non-allowlisted hostname mapped to that same local server. validate reports the Sandbox section as ok only after the effective engine passes its functional proof. Missing or corrupt fail-closed SRT reports waiting/sandbox_unavailable, never host execution.

  • native — every sandboxed command is rewritten to srt --settings <generated-file> <command> .... The generated settings file encodes the network and filesystem policy below. The run-scoped NodeRepl child is prepared through this same path, so evaluated JavaScript does not bypass the policy. Under network.mode: "all" the rewrite instead drives SRT through its library entry (see below) because the SRT CLI always starts domain filtering.
  • off — Bash, Exec and stdio-MCP subprocesses run unwrapped on the host, without requiring an SRT engine, when no retained ProcessJobs roots need protection. Omitting the sandbox block has the same behavior.

Retained ProcessJobs roots are the exception, including roots retained after jobs are disabled. They require fail-closed native SRT protection and explicit private-root denial, unless the validated trusted-host opt-in processJobs.unsafeAllowUnprotectedState with explicit sandbox.mode: "off" deliberately runs them unprotected (see below). A synthetic off/omitted policy confines read/write access to the workspace and uses unrestricted network; a configured native policy retains its readable/writable roots and network policy. When protection is required, missing SRT blocks execution with no host fallback. Enabled jobs register their state root at startup. validate, doctor and status distinguish configured mode from this effective ProcessJobs boundary; read-only validation predicts required protection without initializing state.

Unwrapped same-UID subprocesses can access private agent state, including clear-sessions registry/control directories and coordination leases outside the workspace. Recovery attestation still blocks pending clear-sessions recovery; cooperative ownership is not an OS security boundary against arbitrary shell commands. Clear-sessions strengthens native policies but does not create a sandbox from off/omitted configuration. Fixed subagent verification observations remain read-only and fail closed independently.

Explicit sandbox denial markers in wrapped subprocess output report sandbox_denied with bounded output. Bare Operation not permitted / EPERM errors say the command ran sandboxed and the permission error is commonly a sandbox denial, but may be an ordinary OS permission error; they do not report sandbox_denied. Other ambiguous failures retain sandbox provenance. MCP startup warnings contain only path-free classified summaries, with raw stderr sent to the operator. An ordinary unwrapped missing executable remains a spawn/command-not-found error.

processJobs.unsafeAllowUnprotectedState is a separate, JSON-only trusted-host posture. It requires an explicit sandbox.mode: "off"; omission is rejected. When its additional registry and Pi-only route gates pass, the app suppresses both ProcessJobs and clear-sessions synthetic SRT injection. This makes ProcessJobs state and its operator secret model-accessible while preserving the durable registry, leases, reply-artifact private roots, and provider-zero route gates. See Background process jobs.

sandbox.network.mode controls egress from inside the sandbox:

network.modeBehavior
noneNo network access (default).
localhostLoopback only.
allowlistOnly hosts matching network.allowlist.
allUnrestricted egress; filesystem scopes and deny-write globs stay fully enforced.

Allowlist entries are matched as host suffixes. A leading *. is a wildcard suffix — *.example.com matches api.example.com. There is no CIDR and no port syntax; entries are hostnames/suffixes only. Bare *, IPv6 literals (including ::1), whitespace, paths, and port-bearing entries are rejected. Localhost policy uses the enforceable localhost/IPv4 loopback representation.

Loopback is never implicit in allowlist mode. Enabled app-owned ask tools need 127.0.0.1 or localhost for their interaction bridge, in addition to slack.com for SlackSendMessage and api.telegram.org (or the configured apiRoot) for Telegram tools. Mono-agent scopes SRT’s coarse loopback connect/bind capability to the trusted adapter-send child; ordinary Bash and project MCP commands do not gain loopback binding from that entry. Use network mode localhost only when every sandboxed command intentionally needs local networking (including SRT’s loopback binding capability). The adapter destination allowlist still applies independently. mono-agent validate reports a waiting Tools & MCP section with the missing host before a run.

{
"sandbox": {
"mode": "native",
"network": { "mode": "allowlist", "allowlist": ["*.githubusercontent.com", "registry.npmjs.org"] }
}
}
{ "sandbox": { "network": { "mode": "allowlist", "allowlist": ["*.githubusercontent.com", "registry.npmjs.org"] } } }
  • readableRoots / writableRoots — directories the sandboxed process may read from / write to. Relative entries (like ".") resolve against the workspace root. Both default to the workspace. SRT starts from a global read denial, then reopens these roots plus a reviewed immutable OS/runtime set and narrowly derived executable dependencies; user-managed toolchain data outside those roots must be declared explicitly.
  • denyWrite — write-deny globs applied on top of writableRoots. Relative globs always resolve against the policy workspace root, even when a command runs from a nested cwd. The defaults protect secrets and git internals:
{
"sandbox": {
"denyWrite": [".env", ".env.*", ".git/config", ".git/hooks/**"]
}
}

If mode: "native" but no SRT engine passes its functional proof, the fallback decides what happens:

  • fail-closed (default) — the command is rejected and nothing runs unsandboxed. The sandbox raises sandbox_unavailable internally; what the tool call returns is the outcome code sandbox_prepare_failed, with the text Error: Sandbox engine is unavailable and policy is fail-closed. The run continues — only that command fails.
  • unsafe-host-process — the command runs unwrapped on the host. This requires both fallback: "unsafe-host-process" and unsafeAllowHostProcess: true. Setting fallback to unsafe-host-process without the explicit unsafeAllowHostProcess: true is a config error.
{
"sandbox": {
"mode": "native",
"fallback": "unsafe-host-process",
"unsafeAllowHostProcess": true
}
}

mono-agent start does not silently relax a fail-closed policy. It records the effective sandbox state at startup; mono-agent status prints effective, engine, fallback, and whether the fallback is active. For a fail-closed missing engine, status describes the engine as unavailable with sandbox_unavailable, and the sandboxed commands themselves fail with the sandbox_prepare_failed tool outcome. For an unsafe fallback, start/status include the unsafe warning above so the operator can see that filesystem roots and denyWrite entries are not protecting the host process.

Runtime resolution prefers the managed macOS install and re-hashes its complete tree against the independent release digest before each command. A present-but-corrupt managed install fails closed and never downgrades to a PATH command. A compatible external srt is considered only when the managed path is absent: it is resolved to a trusted canonical absolute file, functionally proved, and pinned by path, filesystem identity, size, and SHA-256. Any later PATH shadow or same-path replacement fails closed. SRT executables and managed install roots may not overlap a configured writable root. Negative availability checks are retried, so sandbox setup/repair can take effect without restarting the agent.

The managed SRT CLI is itself launched by Node before SRT can enforce the configured filesystem policy. Managed setup and status therefore require the selected Node path to be a current-user- or root-owned, non-symbolic regular file that is executable, has no setuid/setgid privilege bits, is not group/world-writable, and has exactly one hard link. The single-link rule is deliberate even though a hard link does not, by itself, grant another OS principal write access.

SRT confines the later workload by pathname while that workload still runs as the same user. For a user-owned Node executable, an otherwise invisible second hard link could name the same owner-writable inode from inside a configured writable root. The workload could then modify the launcher through that alias even though mono-agent proved that the selected canonical path was outside every writable root. There is no portable API to enumerate every hard-link alias. Path/device/inode/hash revalidation detects an earlier byte change, but it cannot prove alias placement or eliminate the final revalidation-to-execution window. Ownership and mode checks alone are therefore insufficient for this path-based sandbox boundary.

The rule is installation-manager agnostic: NVM, Homebrew Cellar, system Node, and hosted toolcache layouts are all compatible when the selected executable is single-link and passes the owner/mode checks. If setup or status reports multiple hard links, select or reinstall a single-link Node installation rather than copying or relinking the binary ad hoc. The private managed SRT marker, lockfile, CLI, package manifest, and dependency tree remain owner-only and single-link as a separate cache-integrity invariant. Node+CLI resolution fails closed on Windows or another platform without POSIX uid ownership checks; managed setup remains a macOS-only capability.

When a request supplies its own sandbox policy, it is merged with the configured baseline so the result is never more permissive than the configured policy. A request-scoped policy can only tighten — it can never widen filesystem access or re-enable host execution:

  • readableRoots / writableRoots are intersected (the request can shrink, not extend, the roots).
  • denyWrite globs are unioned (more denials, never fewer).
  • network access can only narrow.
  • fallback collapses to fail-closed if either side is fail-closed.
  • unsafeAllowHostProcess stays on only if both sides have it on.

This merge is auto (the harness performs it). Constructing request-scoped policies is a code path — see Programmatic.