An AI coding agent that can write code can also write rm -rf. Most of the time the agent is well-behaved, prompt injection isn’t a factor, and the worst thing it’ll do is reformat your code. But the worst-case isn’t zero. Especially for remote sessions where a teammate is running an agent on your dev box, “the agent has the same privileges as you” is not the security model you want.

Ringlet’s remote-session feature wraps the agent in an OS-level sandbox before launch. This article describes how that works, what the agent can and can’t do, and where the limits are.

The threat model

What we’re defending against:

  1. An agent making an honest mistake. It runs rm against the wrong path. The sandbox bounds the blast radius to the workspace.
  2. An agent following a malicious tool result. Prompt injection in a fetched web page or an MCP-server reply convinces it to exfiltrate or destroy data. The sandbox limits what it can reach.
  3. A bug in the agent or one of its tools that lets it do something the user didn’t intend. Same defence.

What we’re not defending against:

  1. A malicious agent vendor. If Anthropic ships a malicious Claude Code, the sandbox slows it down but doesn’t stop it from doing whatever the binary is hard-coded to do.
  2. An attacker with root on the machine. The sandbox is an unprivileged wrapper. Root can bypass it.
  3. An attacker with a kernel exploit. Same.

Ringlet’s sandbox is a trust-but-verify layer for an agent you mostly trust, not a containment layer for an agent you don’t.

Linux: bubblewrap (bwrap)

On Linux, Ringlet wraps the agent in bwrap. The default profile mounts:

  • The system root, read-only.
  • An explicit workspace directory, read-write (typically the project directory you’re running the agent against).
  • The agent’s HOME from the Ringlet profile, read-write.
  • /proc, /sys, /dev — restricted, kernel-default views.
  • A fresh user namespace, IPC namespace, and PID namespace.

What the sandbox bans:

  • Writes outside the workspace and the profile HOME.
  • Binds to network ports (the agent can make outbound connections but can’t listen).
  • Loading kernel modules, mounting filesystems, joining other namespaces.
  • Accessing other users’ processes.

The actual bwrap invocation Ringlet builds looks roughly like:

bwrap \
  --ro-bind / / \
  --bind /home/you/projects/myrepo /workspace \
  --bind /home/you/.ringlet/profiles/work/HOME /home/agent \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --unshare-all \
  --share-net \
  --new-session \
  --setenv HOME /home/agent \
  --setenv ANTHROPIC_API_KEY <from-keychain> \
  --setenv ANTHROPIC_BASE_URL https://api.anthropic.com \
  --chdir /workspace \
  /usr/local/bin/claude

The agent’s view is as if it’s running in a much smaller filesystem. It sees /workspace as its working directory and /home/agent as its HOME. It can read the system binaries it needs (because the root is mounted read-only) but it can’t write to /etc, /usr, or anywhere outside its workspace.

Network access is granted by --share-net — necessary because the agent has to reach the provider API. If you want stricter network control, run the daemon behind a proxy and constrain outbound DNS resolution.

macOS: sandbox-exec

macOS’s equivalent is sandbox-exec, which uses Apple’s Sandbox framework (the same one App Sandbox uses for App Store apps). Ringlet ships a .sb profile:

(version 1)
(deny default)

(allow file-read*)                                 ; system binaries / libs
(allow file-write* (subpath "/Users/you/projects/myrepo"))
(allow file-write* (subpath "/Users/you/.ringlet/profiles/work/HOME"))
(allow file-write* (literal "/private/tmp"))
(allow file-write* (regex #"^/var/folders/.*"))    ; for caches

(allow network-outbound (remote tcp))              ; provider API calls
(deny network-bind)                                ; no listening sockets

(allow process-fork)
(allow process-exec (regex #".*"))
(allow signal (target self))

(deny iokit-open)
(deny system-info)

The agent runs with effectively the same permissions as before but can’t write outside its workspace or its profile HOME, and can’t bind to ports. Apple’s sandbox is fine-grained — see the Sandbox Reference for the full grammar.

Customising the sandbox

Sometimes the default is too strict. The most common case: a project’s test suite needs to bind to a localhost port (e.g. a dev server on :3000). Two options:

  1. Per-profile allow list. Add a [sandbox] block to the profile manifest:
    [sandbox]
    allow-bind = ["127.0.0.1:3000"]
    extra-rw   = ["/var/log/myapp"]
  2. Sandbox bypass for trusted workflows. Set sandbox = "off" in the profile. The agent runs without bwrap/sandbox-exec. We don’t recommend this for remote sessions but it’s there if you need it.

What about local sessions?

By default, ringlet profiles run <name> (no --remote) doesn’t apply the sandbox. The reasoning: a local session is you typing commands at an agent on your own machine — the agent has whatever privileges you do, by definition. Wrapping it in bwrap is theatre.

Remote sessions are different: the daemon is launching the agent on behalf of a WebSocket client, possibly across a network. The trust model is different. So remote always sandboxes, local never does unless you explicitly opt in.

If you want sandboxing on local sessions too, ringlet profiles set work --sandbox always. Trade-off: any tool the agent tries to use that involves a path outside the workspace will fail.

Failure modes worth knowing about

  • Path-mapped tools break. If a tool expects an absolute path that’s outside the workspace, it fails inside the sandbox. Example: an MCP tool that reads /Users/you/Documents/spec.md — Ringlet maps /workspace, not /Users/you. Either move the input into the workspace or extend the sandbox config.
  • Network mounts. SMB / NFS shares outside the workspace mount don’t appear. Mount them inside the workspace or extend.
  • Some Node/Python tools assume access to a global cache. Inside the sandbox, they may try to write to ~/.npm or ~/.cache/pip — which is now the profile HOME, not the system one. Usually fine, occasionally surprising.
  • Long-running processes the agent spawns inherit the sandbox. If the agent starts a dev server in the background, that dev server is sandboxed too. Usually correct.

Auditing what the sandbox blocked

Ringlet’s audit log records sandbox denials when --audit-log is on:

ringlet daemon --stay-alive --audit-log ~/.ringlet/audit.db
# ...later...
ringlet audit query --type sandbox-deny --profile work --since 2026-05-01

This is useful for tuning the sandbox config. “The agent tried to do X and was denied” tells you whether to extend the rules or whether the denial saved you.

How this compares to container-level isolation

You could run each agent in a Docker container. We don’t, because:

  • Cold-start is slow (1–3 seconds for container creation; bwrap is sub-100ms).
  • Container networking adds friction (you have to mount Docker socket, expose ports).
  • The container layer itself is a security boundary, but it also adds attack surface (Docker daemon, image registry, etc.).

For Ringlet’s threat model — limit blast radius of a mostly-trusted agent — bwrap/sandbox-exec hits the right balance. If your threat model is stricter (e.g. running untrusted code, multi-tenant), use containers — and run Ringlet inside them.

Try it

The sandbox kicks in any time you use --remote. The simplest test:

# On your dev box
ringlet daemon --stay-alive

# From your laptop
ringlet profiles run work --remote dev-box.local:8765

# Inside the agent, try:
> rm /etc/passwd
# → Permission denied (rule from sandbox profile)

The denial is the feature. If you’d rather it not be there, opt out per-profile — but for any session you’d let a teammate touch, leave the sandbox on.