← posts / infrastructure

Sandbox your local MCP servers without Docker: Anthropic's srt on Seatbelt and bubblewrap

A 15-minute guide to wrapping a local MCP server in Anthropic's sandbox-runtime (srt): install it, start from deny-by-default, open only the paths and domains the server needs, and know what this research preview does not cover.

if.codesOct 27, 2026 · 6 min read#mcp#sandboxing#security#ai-agentsAI-assisted

A local MCP server is a process your agent starts with your user account. It can read your ~/.ssh, write to your shell profile and call any host on the internet. Most of the time that is fine. The day a server has a bug, a malicious dependency or a prompt-injected tool call, it is not.

The sandbox-runtime creates a secure boundary around local processes.
The sandbox-runtime creates a secure boundary around local processes.

Anthropic's sandbox-runtime (the srt CLI, built for Claude Code) puts a process behind filesystem and network rules using the operating system's own tools: sandbox-exec with generated Seatbelt profiles on macOS, bubblewrap on Linux. There is no container, no image and no daemon. This guide wraps an MCP server in it in about 15 minutes. Everything below comes from the project's README; the project calls itself a research preview and warns that its APIs and config format may change, so check the README if a key stops working.

1. Install srt and its helpers (3 minutes)

npm install -g @anthropic-ai/sandbox-runtime

macOS needs ripgrep:

brew install ripgrep

Linux needs bubblewrap, socat and ripgrep (Debian/Ubuntu package names):

sudo apt install bubblewrap socat ripgrep

On Ubuntu 24.04 and later, unprivileged user namespaces may be restricted by AppArmor. The README says you may need kernel.apparmor_restrict_unprivileged_userns set to 0 or an AppArmor profile for bubblewrap. Windows support exists but is alpha; this guide covers macOS and Linux.

2. See the defaults block things (2 minutes)

With no settings file, srt allows reads, denies network access and denies writes outside a few default paths. Try it:

srt "curl example.com"
Running: curl example.com
Connection blocked by network allowlist

Reads are open by default, which is the part to fix first. Your keys are still readable:

srt "cat ~/.ssh/id_rsa"

That works until you add a denyRead rule. Network is closed, reads are open: keep that in mind for the rest of the guide.

3. Write a deny-by-default settings file (4 minutes)

srt reads ~/.srt-settings.json by default. The file is optional, but if it exists and is empty, unreadable or invalid JSON, srt stops with an error instead of falling back to the defaults. Paths can be relative to the current working directory, and ~ expands to your home directory.

A deny-by-default approach ensures only explicitly permitted access is granted.
A deny-by-default approach ensures only explicitly permitted access is granted.

A starting point for an MCP server that only needs the project folder and no network:

{
  "network": {
    "allowedDomains": [],
    "deniedDomains": [],
    "allowLocalBinding": false
  },
  "filesystem": {
    "denyRead": ["~/.ssh", "~/.aws", "~/.config/gh", "~/.netrc"],
    "allowRead": [],
    "allowWrite": ["."],
    "denyWrite": [".env"]
  }
}

How the rules combine, per the README:

  • Network is deny-all. Only hosts in allowedDomains get through; wildcards (*.github.com) and :port suffixes are supported.
  • Reads are allowed by default. denyRead hides paths, and allowRead overrides denyRead.
  • Writes are denied by default. allowWrite opens paths, and denyWrite wins over allowWrite.

The denyRead list above is an example; add whatever holds credentials on your machine. Some files are always write-protected, even inside an allowed path: .bashrc, .bash_profile, .zshrc, .zprofile, .profile, .gitconfig, .gitmodules, .ripgreprc, .mcp.json, .git/config, and the directories .vscode/, .idea/, .claude/commands/, .claude/agents/ and .git/hooks/. A sandboxed server cannot rewrite its own MCP config or plant a git hook.

Check the file before you wire it in:

srt "cat ~/.ssh/id_rsa"
cat: /Users/you/.ssh/id_rsa: Operation not permitted
Need the backend underneath done properly?I build and harden Go and Python backends. The estimate is free.

4. Wrap the MCP server (3 minutes)

The README's example is the reference filesystem server. Before, in .mcp.json:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem"]
    }
  }
}

After: the command becomes srt and the old command moves into args:

{
  "mcpServers": {
    "filesystem": {
      "command": "srt",
      "args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
    }
  }
}

One practical tweak. npx -y downloads the package at start-up, which needs the npm registry and a writable npm cache, and both are closed in a deny-by-default sandbox. The README also warns that a copy in <project>/node_modules/ inside a writable project can be rewritten by a sandboxed command. So install the server globally, outside the sandbox, and run its binary under srt:

npm install -g @modelcontextprotocol/server-filesystem
{
  "mcpServers": {
    "filesystem": {
      "command": "srt",
      "args": ["mcp-server-filesystem", "."]
    }
  }
}

The . is the directory the filesystem server exposes. srt's allowWrite: ["."] is the outer fence: even if the server's own path checks fail, the OS refuses writes elsewhere. The README's MCP example shows what a blocked write looks like: EPERM: operation not permitted.

Different servers need different rules. Give each its own file with --settings (use an absolute path, since nothing expands ~ inside .mcp.json):

{
  "mcpServers": {
    "filesystem": {
      "command": "srt",
      "args": ["--settings", "/Users/you/.srt/filesystem.json", "mcp-server-filesystem", "."]
    }
  }
}

5. Open the network one host at a time (2 minutes)

Start with allowedDomains: [] and add hosts only when the server fails without them. A server that talks to the GitHub API needs the API host, not all of GitHub:

Opening network access one host at a time minimizes the attack surface.
Opening network access one host at a time minimizes the attack surface.
{
  "network": {
    "allowedDomains": ["api.github.com"],
    "deniedDomains": []
  }
}

The README warns about allowing broad domains like github.com: a host where anyone can push content is a way to send data out. Find what a server actually calls with debug logging:

srt --debug curl https://example.com

Clients that honour proxy settings get a 403 with X-Proxy-Error: blocked-by-sandbox-runtime from srt's proxy for blocked hosts, and the block is recorded as a violation. Two more details: an allowed hostname that resolves to loopback, link-local, cloud metadata or a local-interface address is refused, and allowLocalBinding stays false unless the server must listen on a port (it is ignored on Linux).

What srt does not protect against

Read this section before you rely on it. All of it is from the README.

  • It is a research preview. Config keys and behaviour may change between releases. Pin a version if a team depends on it.
  • The network filter stops direct connections, not determined code. The README's own words: treat it as stopping commands that connect directly, "not code written to defeat it". Traffic goes through host-side proxies, and on Linux the proxy environment variables can be ignored by programs that do not respect them (those connections are blocked, not leaked, but expect odd failures).
  • Unix sockets are a way out. They are blocked by default. Allowing one can hand the sandbox a powerful service; the README says allowing /var/run/docker.sock would effectively grant access to the host system. On Linux, path-based socket allowlists are ignored because seccomp cannot filter by path.
  • Linux glob gaps. allowWrite and denyWrite take literal paths only, some denyRead patterns are skipped (and a skipped pattern hides nothing), and denied directories can show up as empty writable tmpfs mounts inside the sandbox. Use plain paths on Linux and test each deny.
  • Nested sandboxes break. Nested user namespaces are blocked on Linux, so browser sandboxes, rootless containers and nested bubblewrap fail. Turning on allowNestedUserNamespaces weakens the write-deny guarantees.
  • Some options remove the protection. enableWeakerNetworkIsolation opens a possible exfiltration path through trustd on macOS, and allowAppleEvents lets a sandboxed command launch any application with no prompt. Set these only in your own user-level config, never from a project file.

None of this makes srt a reason to run servers you would not otherwise trust. It is a cheap second fence: a server with a path-traversal bug or a hijacked dependency hits the OS's refusal instead of your keys, and a server that tries to reach a host you never listed gets blocked.

Sources

if.codesI build RAG, AI integrations and agent pipelines on Go and Python backends — and write about it here.
// keep reading

More posts on AI and backends.

// free quote

Read something you need? I’ll quote it for free.

RAG, AI integrations, agents or the backend underneath — tell me what you have and what should change. I read every request myself.

Free · no commitment

Tell me what you have. I’ll tell you what it takes.

1Describe the projectA few sentences is enough — about two minutes.
2I review itI read it myself and may ask a follow-up question.
3You get a free quoteScope, approach and estimate — yours to keep, no strings.
What kind of project is it?
Free and without obligation. Your details are used only to reply — see the privacy policy.