OS Sandbox
UGENT confines every process it spawns using OS-level primitives: sandbox-exec (Seatbelt) on macOS and bubblewrap on Linux. The kernel enforces the boundary for the whole process tree — not a pattern list.
Document Author: Uni Zhu
Why This Layer Exists
UGENT's application-layer defences — the injection firewall, the tool security validator, the capability classifier, the confirmation flow — all reason about tool calls. None of that survives contact with a shell: once bash is running, a command none of those layers anticipated can read anything you can read and write anything you can write.
The sandbox is the layer underneath. It does not replace the layers above it — the firewall still catches what the sandbox cannot see, and the validator still gives better error messages for structured tool calls. This is defence in depth, not a substitute.
How It Works
Containment is applied as a command transformation before the process is spawned:
bash -c "..." -> sandbox-exec -p <profile> bash -c "..." # macOS
bash -c "..." -> bwrap <flags> -- bash -c "..." # LinuxThe sandbox is process-wide and applies at every spawn site: the bash tool (piped and PTY paths), background jobs, skill shell substitution, autorun commands, MCP servers over stdio, delegated claude/codex CLIs, and channel plugin workers.
Platform Support
| Platform | Backend | Notes |
|---|---|---|
| macOS | Seatbelt (sandbox-exec) | Built in. Nothing to install. |
| Linux | bubblewrap (bwrap) | Install it: apt install bubblewrap or dnf install bubblewrap. Version 0.5.0 is the floor; 0.8.0 or later additionally blocks nested user namespaces. |
| Windows | none yet | auto warns and runs unconfined; required refuses to start. |
The startup probe asserts enforcement, not presence
On startup UGENT runs a fast probe (~30 ms, once) that proves containment actually works: a permissive profile must launch, and an added read-deny must actually cause a denial. On Linux the probe runs the real generated argument list, so a flag missing from the builder fails at startup rather than silently leaving the sandbox escapable. /sandbox status reports the effective state, so a failed probe is visible instead of assumed.
Spawn Classes
Each class of spawned process gets its own policy, because trust levels and legitimate filesystem needs differ. One global profile cannot serve all five.
| Class | Covers | Writes | Credential reads | Network |
|---|---|---|---|---|
bash | The bash tool and its background jobs | workspace, temp dirs, build caches | denied | allowed by default |
tooling | Skill shell substitution, autorun gate | workspace, temp dirs, build caches | denied | allowed by default |
mcp_server | Third-party MCP servers over stdio | ~/.ugent/mcp only | denied | allowed by default |
external_agent | The claude and codex CLIs | ~/.claude, ~/.codex, build caches | denied | allowed by default |
plugin | Channel plugin workers | plugin state dirs only — no workspace | denied | allowed by default |
Design notes worth knowing:
- External agents keep their own config directories writable because denying them would simply break the CLIs. They do not get a credential-read exemption: delegated agents are prompt-injectable in their own right, and exempting them would turn delegation into a bypass for anything
bashcannot read. - Plugins get blast-radius containment, not full containment. A channel plugin is third-party code from a public ecosystem holding channel credentials, and it needs the network to reach its platform. What the profile buys: one hostile plugin costs one channel rather than every credential on the machine. Exempt an individual plugin with
sandbox = falseon itsplugins.tomlentry. - Some paths are denied back out for every class, even inside write grants:
PATHdirectories inside build caches (~/.cargo/bin,~/.rustup/toolchains,~/.local/bin,~/.npm/_npx),~/.ugentitself, and the config manifests that name a binary to spawn (plugins.toml,mcp.toml,cron.toml, ...). A write to those becomes unconfined execution later, so they are not writable by anything confined. - Credential directories are read-denied at the OS level (
~/.ssh,~/.aws,~/.gnupg,~/.kube,~/.docker,~/.config/gh,~/.config/gcloud,~/.gcp,~/.azure), derived from the same sensitive-path patterns the tool validator uses — with the difference that a shell command cannot walk past it. On Linux this list is enforced only at the tool layer (see Linux limitations).
Modes
| Mode | Backend available | Backend missing or probe fails |
|---|---|---|
off | no containment | no containment |
auto (default) | confined | warn and continue unconfined |
required | confined | refuse to start |
required exists so that a configuration claiming to sandbox cannot silently degrade to no enforcement. Use it when running unconfined is worse than not running.
Configuration
sandbox.toml is discovered like the other overlay files: workspace root, then <workspace>/.ugent/, then ~/.ugent/. Missing file means defaults (auto, network allowed).
mode = "auto" # off | auto | required
network = "allow" # allow | deny
# Widen writes for toolchains that write elsewhere. Entries must start with
# ~/ or / — bare relative paths are rejected.
extra_write_allow = ["~/.pnpm-store"]
# Add to the OS-enforced credential read-deny list (never replaces it).
extra_read_deny = ["~/company-secrets"]
# Per-class overrides. Lists ADD to the global values; network REPLACES it
# for that class only.
[profiles.mcp_server]
network = "deny"
[profiles.bash]
extra_write_allow = ["/opt/build-cache"]Not hot-reloaded, deliberately
Unlike the other overlay files, sandbox.toml is not hot-reloaded. Re-reading it on a file change would let anything able to write the file relax containment before the next spawn. Applying an edit is an explicit human action: /sandbox reload, or a restart.
Workspace overlays can only tighten
A sandbox.toml inside a workspace is attacker-influenced — cloning a repository must not be able to disable your sandbox. A workspace-sourced file may tighten the mode, deny the network, and add read denials. Every write widening in it is dropped, and any attempt to loosen the mode or re-enable the network is ignored, with a warning. Put real widenings in ~/.ugent/sandbox.toml, which is yours.
Channel remote control
By default, channels (the web UI, chat plugins) may only inspect the sandbox. To let a channel change it, set:
allow_remote_control = false # default. true lets a channel turn it off.The boundary lives in the host config on purpose: a gate in a frontend's own UI cannot enforce anything against someone holding the API key. Because the sandbox is process-wide, a channel turning it off would also remove containment for your local terminal, cron jobs, and every other channel — so disabling from a channel additionally requires a confirmation token written to the host log, and a host in required mode refuses the downgrade outright.
CLI Flags and Commands
ugent --sandbox # force auto
ugent --no-sandbox # force off
ugent --sandbox-required # refuse to start unconfinedThe flags are mutually exclusive and outrank sandbox.toml in both directions — an operator at the terminal is more trusted than a file, and --no-sandbox must work as an escape hatch even when a config demands containment.
| Command | Description |
|---|---|
/sandbox | Effective sandbox status (defaults to status) |
/sandbox status | Effective state and per-class policy summary |
/sandbox profile <class> | Resolved policy for bash, tooling, mcp_server, external_agent, or plugin |
/sandbox test | Prove containment is enforced right now |
/sandbox reload | Re-read sandbox.toml and rebuild |
/sandbox on / /sandbox off | Toggle for this session only |
Human-only
/sandbox is a REPL and TUI command, never an LLM tool — the model cannot turn the sandbox off, the same rule as /firewall approve.
/sandbox test is worth calling out. It attempts a write to an existing root-owned file and distinguishes the two failure modes: EPERM ("Operation not permitted") means the sandbox denied it, while EACCES ("Permission denied") means ordinary file permissions did and the sandbox is not enforcing. It writes nothing.
What Cannot Turn It Off
- The model cannot.
/sandboxis never exposed as an LLM tool. - A file write cannot.
sandbox.tomlis in the write-protected config list alongsideugent.tomlandfirewall.toml, and it is not hot-reloaded — even a successful write does nothing until a human reloads. - A workspace cannot. Workspace overlays may only tighten.
- A confined process cannot. On macOS, Seatbelt profiles compose monotonically: a child can re-invoke
sandbox-execbut only narrow, never widen. On Linux the remount escape is closed by an unshared user namespace plus--disable-userns, and the startup probe verifies the real argument list.
Limitations
Stated honestly, because a security boundary you misunderstand is worse than none:
- Linux does not enforce read denials at the OS layer. bubblewrap cannot express "deny reads under
~/.ssh" without inverting the whole policy into a read-allowlist.extra_read_denystays tool-layer-enforced there, and/sandbox profilemarks the difference rather than implying protection that is not there. - Network is binary (
allowordeny), with no domain allowlist — neither backend can express one. It defaults toallowso builds and package installs keep working. Phase 1 gives filesystem containment, not egress control. - Reads are permissive by default on macOS too. The profile is allow-by-default with writes and credential reads denied on top; a deny-by-default read policy breaks in new ways with each macOS release.
/etcstays readable — denying it breaks TLS, becausecurlcannot read/etc/ssl/cert.pem.- A hostile plugin still exfiltrates its own credentials. It is handed them and it has the network. The plugin profile bounds blast radius; it does not contain a malicious plugin.
- Not a substitute for isolation. For genuinely untrusted work, run UGENT inside a container or VM. This layer confines the processes UGENT spawns; it does not confine UGENT itself.
See Also
- Security & Firewall — the layers above: injection firewall, tool policy, rate limiting
- Vault & Secrets — credential storage the read-deny list protects
- Feature List — where the sandbox sits in the security stack