Running code in a sandbox

Run shell commands and code isolated from the real machine: what your platform can enforce, the enforcement switches, disk read modes, port rules, restricting egress to one site, activity tracing and the sbx_ toolset.

Sandbox runs shell commands and source code separated from the real machine: the code runs, but it cannot read your documents, cannot write outside its own directory, and cannot reach the internet unless you allow it.

You manage it under Plugins → Sandbox. Agents in a chat use it through the mcp__senclaw-sandbox__sbx_* toolset.

This is a control in the daemon, at runtime. It is unrelated to what the registry checks when a package is published — see Security model for that side.

Two things share this machinery; do not mix them up:

ForManaged at
The sandbox engine (most of this page)code an agent runs: Bash commands, Python/Node, scheduled scriptsPlugins → Sandbox
Per-Space-App sandboxan installed, long-lived app process serving a portPlugins → Space Apps → the Sandbox button — see Sandboxing a Space App

What your machine can isolate with

The Available isolation card at the top of the page tells you:

PlatformIsolationNote
macOSSeatbelt (sandbox-exec)The most precise — filters both files and individual network ports
LinuxbubblewrapGood for files; cannot filter outbound ports
WindowsAppContainer + Job ObjectImplemented but not yet verified on real hardware
Any platformDockerNeeds a running Docker daemon; adds CPU/RAM limits

If the machine has no isolation tool available, the state is Degraded — code still runs, but with no operating-system barrier at all. The page says so plainly rather than pretending.

Note that Docker capability is probed by asking the daemon, not by checking whether the docker binary exists: a machine can have a working CLI and a broken Docker Desktop, and a probe that stops at "is the binary there" reports Docker as usable right up until every sandbox dies at run time.

The enforcement switches — the most important part

By default the sandbox is merely a tool that is available. The switches on the Security enforcement card turn it into something mandatory for execution paths that already existed:

SwitchDefaultOnOff
Agent shell (Bash)OFFEvery Bash command the agent runs goes through the sandbox, able to write only inside the chat's working directoryThe shell runs directly on the machine, as before
↳ Network / Disk read / Local portson / open / emptyTune the enforced shell separately
Run PythonONPython runs in the sandboxPython is refused — the agent is told "switched off (Plugins → Sandbox)"
Run Node.jsONNode runs in the sandboxNode.js is refused
↳ Code networkOFFLets the Python/Node REPL reach the network
Scheduler scriptsOFFscript and script-agent scheduled tasks run in a single-use sandboxScripts run directly, as before

Three things to keep in mind:

  1. There are two different meanings of "off". Turning off Agent shell or Scheduler scripts returns to the old behaviour (run directly). Turning off Run Python/Node is a flat refusal — there is no alternative path, because those runtimes never ran outside the sandbox in the first place.
  2. If a switch is on and the machine cannot isolate, the command fails — it does not quietly run unsandboxed. That is deliberate: a security switch that releases itself under pressure is not a security switch.
  3. Turning on "Agent shell" is the largest change available here — it stops the agent writing anywhere outside the open project directory. If your workflow needs the agent to touch files outside it, that workflow breaks; turn this on when you actually want the restriction.

What the sandbox can read from disk

Three modes. The default is set on the Defaults for new sandboxes card and changed per sandbox with the sbx_fs_mode tool:

ModeCan read
strict (default)the sandbox's own directory + directories you mounted + system libraries
allowlistthe above, plus the directories listed on the Allowlist card
openthe whole disk, except ~/.ssh, ~/.aws, the Keychain and ~/.senclaw

Writes are always jailed to the sandbox directory, in all three modes.

System libraries (/usr, /System, /opt/homebrew…) stay readable even under strict. That is not an oversight — that is the interpreter: Python lives there, the standard library lives there, the dynamic linker's cache lives there. A read-jail that excludes them is not isolated Python, it is Python that will not start. What strict really cuts off is your data: documents, projects, other apps' files, the rest of $HOME.

⚠️ One exception that catches people: the agent's shell, when enforced, defaults to **open**, not strict. The reasoning is that the agent can already read the whole disk through its own Read tool, so clamping down only the shell path breaks workflows without closing a door. Change it in the Disk read field next to that switch.

Network and ports

The network switch is blunt — on or off. For anything finer, sbx_ports takes three lists, because they are three different permissions:

  • **listen: [8000] — the sandbox may bind** that port, and you can reach it at http://127.0.0.1:8000. This is how you run someone else's app inside a sandbox and open it in a browser. Ports must be ≥ 1024.
  • **connect: [443] — the only remote** port it may dial out to. connect: [443] means "may speak HTTPS".
  • **loopback: [8899] — services on this very machine it may call. Empty is the default, and empty means none.**

**You do not need to switch network on** — the port rules are the whole permission. That is the point: an app serving on 8000 does not thereby earn the right to phone home.

Three things that are easy to get wrong:

  • **connect is per port, not per website.** connect: [443] means every site on 443, not one site. The macOS sandbox cannot express "only this host" — see the next section.
  • Services on your own machine are blocked even with the network on. Deliberately: SenClaw's own REST API on loopback has no password, so a sandbox that can call it can simply ask the daemon to read the file it was forbidden to read — and to create it a second, unrestricted sandbox. This was demonstrated against a live daemon before the rule existed. When you do need a local service, name its port in loopback.
  • On Linux and Docker, opening a listening port gives the sandbox network access — neither platform can filter egress. A container with --network none cannot publish anything, and bubblewrap with --unshare-net has no route back to the host. The tool's response says so in its note field; do not skip it.

Which backend enforces what:

Backendlistenconnectloopback
macOS Seatbeltexact, per portexact, per portexact, per port
Dockerpublished to 127.0.0.1, exactcannot filtercannot filter
Linux bubblewrapworks, but loses the network namespacecannot filtercannot filter

Published ports are always bound to 127.0.0.1, never 0.0.0.0: a bare -p 8000:8000 would expose the sandboxed app to the whole LAN.

DNS on macOS does not travel over port 53

connect: [53, 443] and still no name resolution is a measured result, and the cause is not the port rules: getaddrinfo does not send UDP itself, it asks mDNSResponder over a Unix socket. The generated profile therefore grants that socket — but only to a sandbox that already has outbound permission (network: true, or a non-empty connect), never to one with the network off. A resolver is a data-exfiltration channel in its own right: you can encode data into domain names.

Restricting a sandbox to ONE website

Port rules cannot do this (above).

For a Space App this is built in. Pick only these sites in the app's Sandbox dialog and list the domains — SenClaw stands up the allowlist proxy for you. See Sandboxing a Space App.

For an agent sandbox (sbx_*), build it yourself with this verified recipe:

  1. Run an HTTP proxy with an allowlist outside the sandbox — it decides which domains may pass.
  2. Give the sandbox connect: [] (no direct egress) and loopback: [<the proxy's port>].
  3. Set HTTPS_PROXY=http://127.0.0.1:<the proxy's port> in the sandbox environment.

Anything that ignores the proxy hits the sandbox wall — it fails closed. And because there is no connect, the sandbox has no resolver either, which closes the DNS-tunnel route as well.

Running a real app inside a sandbox, by hand

If it is an installed Space App, do not do this by hand — use Sandboxing a Space App, which handles the whole lifecycle. This section is for stuffing some arbitrary app into a sandbox session.

Two things will cost you an afternoon if you do not know them up front (both measured):

  • **Start background servers as ( cmd < /dev/null > log 2>&1 & )**, not a bare cmd &. A bare & keeps the exec call running until its deadline and then the whole process group is killed — taking the server with it.
  • Do not mount the app's directory read-only and expect the app to run. Anything that writes next to its own code (SQLite, lock files, caches) dies. Copy the app into the sandbox's writable workspace and mount only data read-only.

Activity tracing — and its limits

Turn it on with sbx_trace, read it with sbx_events: which files the code read and wrote, which processes it started, what it connected to. Exactly what you want when reviewing "what does this code actually touch".

⚠️ This is not security evidence. The hooks run inside the sandbox and the log is a file in that same sandbox. Code that means to hide can remove the hook, edit the log, or write straight to a raw file descriptor without touching any traced API. A clean log does not mean safe. The boundary that actually holds against hostile code is the sandbox itself, enforced by the kernel.

Managing sandboxes in the UI

The Managed sandboxes card lists what exists: backend, read mode, network, limits, state, last use. Actions: Stop all (kill every process), Stop container, Delete (keep files), Delete with files. Expand a row to see running processes (PID, %CPU, RAM) and kill them individually.

The Recent runs card shows the last 30 runs with an Isolation column — the isolation actually applied to that run. This is where you check whether the enforcement switches are really doing something.

The Space Apps — per-app sandbox card lists every app with a server process: the mechanism the running process actually got (not the saved configuration), its read mode, its network mode with the count of proxy denials, pid / uptime / launch count, and CPU/RAM. Ten apps per page, sortable, with three buttons per row: detailed monitoring, sandbox configuration, restart.

That first column catches something no other screen shows: an app configured with a sandbox but currently running without one (the profile is fixed at launch time) — that row reads needs restart.

This page deliberately does not do: create sandboxes, mount directories, run commands, change ports, terminals, file browsing. Those go through the agent (MCP tools) or REST.

Asking an agent to do it — the 22 tools

Plain language is enough ("run this Python in isolation", "run this app in a sandbox and open port 8000"). The sandbox-runner skill steers the agent to the right tool:

  • **sbx_run is the default** — a single-use sandbox that deletes itself when finished. Do not create a long-lived sandbox for one calculation.
  • Multi-step work that needs files or installed packages to persist: sbx_createsbx_file_writesbx_run_in / sbx_execsbx_installsbx_delete.
  • The rest: sbx_capabilities, sbx_list, sbx_update, sbx_files, sbx_file_read, sbx_stats, sbx_kill, sbx_mount, sbx_unmount, sbx_fs_mode, sbx_settings, sbx_ports, sbx_trace, sbx_events, sbx_runs.

REST (/api/sandbox/*) and the MCP tools go through the same runner, so a limit enforced on one side is enforced on the other. If the engine is broken the whole /api/sandbox branch answers 503 rather than disappearing — a 404 would be misread as "an older version".

Troubleshooting

SymptomCause / fix
"Python execution is switched off (Plugins → Sandbox)"The Run Python switch is off. Turn it back on under Plugins → Sandbox; there is no alternative path
The agent's Bash commands started failing after enabling enforcementEither the machine cannot isolate (check the Available isolation card), so commands fail instead of running unsandboxed — or the command is writing outside the project directory
An app in the sandbox cannot resolve domain namesThe sandbox has no outbound permission. The resolver is granted only when network is on or connect is non-empty — opening connect: [53] is not the fix on macOS
An app in the sandbox cannot reach a service on the machineWorking as designed. Name that port in loopback
A server started inside the sandbox dies immediatelyA bare cmd & — see the section on running a real app
Docker reported unusable even though docker --version worksThe engine asks the daemon, not the CLI. Start Docker Desktop and press re-check
A tiny snippet takes seconds to runNormal is about 0.03 s. If it takes seconds, check whether Docker Desktop is hanging

Further reading

In the SenClaw repo

The engine is src/sandbox/ (17 modules): fsmode.rs for read modes, ports.rs for port isolation, policy.rs for the enforcement switches, runner.rs as the single gate every entry point passes through, trace.rs, monitor.rs, mounts.rs and backend/ for Seatbelt, bubblewrap, AppContainer and Docker. It serves MCP over stdio as senclaw sandbox-server, and the agent skill is skills/sandbox-runner/SKILL.md. There is also a standalone Space App copy of the engine under apps/sandbox/ with the same 22 tools but without the enforcement and loopback-isolation work.