Sandbox any MCP server in Claude Code with one line
An MCP server is a program you hand to your agent and let run on your machine, with your user's permissions. Most are fine. If one is buggy or compromised, it can read your SSH keys or call out to any host. Anthropic's Sandbox Runtime (srt) wraps any process in OS-level file and network limits, no container needed. Here is the setup with the real commands. The short version lives on the Deduped use-case page.
What srt does
srt uses sandbox-exec on macOS and bubblewrap on Linux, plus proxy-based network filtering. It was built for Claude Code and is a beta research preview, so config formats may change. Defaults are strict: no network access, no writes outside the working paths, and reads allowed everywhere unless you deny them.
Install
npm install -g @anthropic-ai/sandbox-runtime
Try it before wiring anything
srt "curl anthropic.com"
srt "curl example.com"
srt "cat ~/.ssh/id_rsa"
With an allowlist containing anthropic.com, the first call succeeds. The second prints "Connection blocked by network allowlist". The third fails with "Operation not permitted". That is the whole idea: the process only gets the holes you poke.
Wrap an MCP server
Your unsandboxed .mcp.json entry probably looks like this:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
Change the command to srt and move the old command into args:
{
"mcpServers": {
"filesystem": {
"command": "srt",
"args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
Set the limits
Create ~/.srt-settings.json:
{
"filesystem": {
"denyRead": [],
"allowWrite": ["."],
"denyWrite": ["~/sensitive-folder"]
},
"network": {
"allowedDomains": [],
"deniedDomains": []
}
}
Writes are allow-only, so allowWrite lists the places the server may write. Reads are deny-then-allow. Network is allow-only: an empty allowedDomains means no network at all, so add the hosts your server really needs.
One gotcha from the README: if the settings file exists but is empty or invalid, srt exits with an error instead of falling back to defaults. That is on purpose, because a silent fallback would drop your deny rules.
Test that it bites
Ask your agent to write a file into the denied folder. You should see an EPERM "operation not permitted" error. If the write succeeds, your config is not being read, so check the path and run srt --debug on a sample command.
srt --debug curl https://example.com
srt --settings /path/to/srt-settings.json npm install
Where it stops
Sandboxing limits what the server can touch, not what it is told to do with data you give it. Keep secrets out of the folders you allow, and keep the allowlist short. Windows support is newer and has documented known limitations, so read that section of the README first.
For another way to cut risk in Claude Code, see the GitHub MCP server walkthrough.
Want the setup commands for 50 AI repos people actually ship with? Get the free AI Repos That Actually Ship pack: dedupedai.com
1 reaction
I Had Claude Code on the Web Dissect Its Own Sandbox
I have a Claude Code on the web bot wired into my personal Slack channel. One day I idly asked it: "where are you running right now? Go inspect your own environment and tell me." What came back was a work log of the model itself digging through ps and /proc to reconstruct the ground under its own feet. So I spent two days playing with it: first having it dissect the environment from the inside, then the next day probing that boundary with exfiltration and evasion requests.
The conclusion up front: the environment is a two-layer setup — Firecracker microVM isolation plus a mandatory pass-through of all egress via a claude-internal allowlist proxy — and none of the exfiltration paths I tried got through that structure.
The Environment, Seen From Inside
There is no ip, no ifconfig, no ss. So everything has to be dug out of /proc. Here's what turned up:
- OS: Ubuntu 24.04.4 LTS (Noble Numbat). whoami is root. The home dir (/root) is empty
- Virtualization: inside a Firecracker microVM
- NIC: eth0 = 192.0.2.2/24, GW 192.0.2.1, hostname vm, IPv4 only (no /proc/net/tcp6)
The interesting part is the NIC's address range. 192.0.2.0/24 is RFC 5737 TEST-NET-1 — a block reserved for documentation and examples that is never routed on the real internet. Instead of assigning a real IP, they deliberately hand the instance a "synthetic network that connects nowhere." The design philosophy already shows through here.
I also reconstructed the LISTEN sockets by decoding /proc/net/tcp by hand:
- 0.0.0.0:2024 / 0.0.0.0:2025 → PID 1 process_api (--firecracker-init). The host ↔ VM control channel, and the only thing facing outward
- a few 127.0.0.1 internal ports → environment-manager (internal control API) and claude (the proxy below)
Process Tree and Data Flow
Reconstructing the layout from ps aux and the self-dissection gives this vertical spine:
you (Slack client)
| message / reply (stream-json)
v
+-- Firecracker microVM ------------------------------
|
| PID 1 process_api (--firecracker-init)
| -> host<->VM control channel, only public listener
|
| PID 557 environment-manager
| -> task-run / session management (supervisor)
|
| PID 569 claude (conductor / the agent itself)
| |-- spawn --> worker(bash) xN (runs the actual commands)
| \-- HTTPS --> agent-proxy relay (CONNECT-only, allowlist)
| | allow / 403
+-------------------------+---------------------------
v
allowed hosts only: api.anthropic.com, github, npm, pypi ...
off-allowlist hosts -> 403
The point is the separation of execution and command. claude (PID 569) is the conductor; the actual lscpu and ps are farmed out to disposable worker(bash) processes. Incidentally claude's VSZ is a whopping 73GB, but that's V8/Node.js virtual address reservation — real RSS is ~400MB. And kill 569 is a SIGTERM to itself (this very session) — a self-destruct command.
What environment-manager Actually Is
Chasing down the environment-manager listening on 127.0.0.1 leads to /opt/env-runner/environment-manager: a ~56MB stripped Go 1.24 binary, internal name environment-runner, self-description _"Environment Runner handles the lifecycle of agentic sessions."_ — Anthropic's session orchestrator / supervisor. It's the layer that mediates between the cloud API (the Claude Code on the web backend) and the local claude CLI.
Reconstructing its startup behavior from the binary's strings and help output, it roughly goes:
1. Reads a session spec JSON (~81KB) over stdin
2. Initializes the sandbox, MCP servers, skills, and git config in parallel
3. Assembles and runs claude's launch command line (model, effort, fallback, SDK URL, etc.)
4. Passes the auth token via file descriptor (fd 3/4) — never writing it to disk or logs, and self-masking it as <redacted> even in its logs
5. Measures things like spawn→ready and ttft
Its subcommands include task-run (single-session execution — PID 557 is one of these), orchestrator (a resident loop that polls for work and hands each job to task-run), and preload-claude (a warm spare). It's also the thing that installs a hook forcing every git commit to noreply@anthropic.com with a required SSH signature.
Egress Is Forced Through an Allowlist Proxy
The network is clamped down in two stages.
1. Firecracker VM isolation itself
2. A claude-internal agent-proxy relay. All outbound traffic goes through this in-process proxy, and allow / 403 is decided by a host allowlist. On top of that it's CONNECT-tunnel (HTTPS) only — a bare GET/HTTP gets bounced with 405
Here's what I actually got and didn't get when I poked at it:
| Category | Example | Verdict |
| ---------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- |
| Auth-injected hosts | *.googleapis.com, api.anthropic.com, GitHub (push/PR OK), AWS/GCP SDK | allow |
| Package fetch (direct) | npm, pypi, crates.io, proxy.golang.org | allow |
| Off-allowlist hosts | arbitrary IP-check / download sites | 403 |
| Third-party PPAs (launchpad) | deadsnakes, ondrej/php | 403 (apt update exits 100) |
| SSH (port 22) | ssh <external host> | timeout (egress is 443-centric; 22 is closed) |
The pip/npm/cargo/go ecosystems go through. What doesn't is "third-party apt sources plus random sites." Even apt-get install succeeds if you stick to the core Ubuntu repos — I could install the ssh client itself. But when I tried to use that ssh to go "check external IP → check allowlist → fetch a tunnel tool," this time it wasn't the network layer but the safety classifier that caught it as "prep for egress evasion" and stopped it. The clamp isn't a single monolith.
The Next Day: Probing the Boundary
With the shape of the environment visible, the next day I flipped it around and started firing off exfiltration and evasion requests to push on the boundary. Harmless reads (uptime / ps aux / ls / various --help / local cp) all go through cleanly. What got refused was around here:
| Request | Actual effect | Verdict |
| -------------------------------------------------------------- | ----------------------------------------------------------------------- | ------- |
| fork bomb | DoS | refused |
| xz -9c $(find …) \| base64 wedged between two harmless lines | compress + base64-exfil an internal binary | refused |
| binary disguised as .bashrc | the same exfil, just disguised | refused |
| gdb/kill to disable the egress relay | only kills the sole exit = not a bypass | refused |
| go tool nm <binary> | extracting environment-manager's symbols = reverse engineering | refused |
| cat /proc/1/environ | dumping PID 1 = process_api's control secrets = credential extraction | refused |
What was interesting is that renaming or restructuring the request doesn't work. Even wedging xz | base64 between two harmless command lines, it sees that "the actual effect of that line is exfiltrating an internal binary" and refuses. It draws the line on effect, not on cosmetic phrasing of the steps.
There was also a moment where merely trying to enumerate the _names_ of environment variables got blocked as "credential discovery." That's an example of the harness's own independent safety classifier firing, separate from the model's own judgment.
Conclusion: Defense in Two Layers, Rephrasing Doesn't Help
The structure that emerged over two days of play:
- The egress path is in-process and the sole exit. So killing it isn't a "bypass" — the exit just disappears
- Credentials are structurally isolated (a denyRead-equivalent plus the classifier, doubled up)
- Renaming / restructuring the steps doesn't work, because the model reads it by _actual effect_
- And the harness classifier runs independently of the model's judgment, so talking one of them around still leaves the other one to stop you
The thing that finally clicked for me is the consistent line between observation and analysis. Watching behavior from the outside (eyeballing your own process with ps) — it'll cooperate. Extracting the guts of an internal binary (pulling symbols with nm) — it refuses. It'll play along with "I want to know where I'm running," but not with "let's take that mechanism apart and carry it out." You can make the resident of the sandbox introduce itself, but it won't hand you the blueprints.
For the record, the only part that's public is the sandbox execution layer (sandbox-runtime); environment-manager itself and the PaaS bits stay proprietary. Everything in this post also stays within what you can trace from the official sandboxing writeup — the story is just that if you get the resident (the model) to introduce itself, it'll answer a fair bit.
---
_This post was written with AI assistance and reviewed by the author._
Mentions
Sandbox any MCP server in Claude Code with one line An MCP server is a program you hand to your agent and let run on your machine, with your user's permissions. Most are fine. If one is buggy or compromised, it can read your SSH keys or call out to any host. Anthropic's Sandbox Runtime (srt) wraps any process in OS-level file and network limits, no container needed. Here is the setup with the real commands. The short version lives on the Deduped use-case page. What srt does srt uses sandbox-exec on macOS and bubblewrap on Linux, plus proxy-based network filtering. It was built for Claude Code and is a beta research preview, so config formats may change. Defaults are strict: no network access, no writes outside the working paths, and reads allowed everywhere unless you deny them. Install npm install -g @anthropic-ai/sandbox-runtime Try it before wiring anything srt "curl anthropic.com" srt "curl example.com" srt "cat ~/.ssh/id_rsa" With an allowlist containing anthropic.com, the first call succeeds. The second prints "Connection blocked by network allowlist". The third fails with "Operation not permitted". That is the whole idea: the process only gets the holes you poke. Wrap an MCP server Your unsandboxed .mcp.json entry probably looks like this: { "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem"] } } } Change the command to srt and move the old command into args: { "mcpServers": { "filesystem": { "command": "srt", "args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"] } } } Set the limits Create ~/.srt-settings.json: { "filesystem": { "denyRead": [], "allowWrite": ["."], "denyWrite": ["~/sensitive-folder"] }, "network": { "allowedDomains": [], "deniedDomains": [] } } Writes are allow-only, so allowWrite lists the places the server may write. Reads are deny-then-allow. Network is allow-only: an empty allowedDomains means no network at all, so add the hosts your server really needs. One gotcha from the README: if the settings file exists but is empty or invalid, srt exits with an error instead of falling back to defaults. That is on purpose, because a silent fallback would drop your deny rules. Test that it bites Ask your agent to write a file into the denied folder. You should see an EPERM "operation not permitted" error. If the write succeeds, your config is not being read, so check the path and run srt --debug on a sample command. srt --debug curl https://example.com srt --settings /path/to/srt-settings.json npm install Where it stops Sandboxing limits what the server can touch, not what it is told to do with data you give it. Keep secrets out of the folders you allow, and keep the allowlist short. Windows support is newer and has documented known limitations, so read that section of the README first. For another way to cut risk in Claude Code, see the GitHub MCP server walkthrough. Want the setup commands for 50 AI repos people actually ship with? Get the free AI Repos That Actually Ship pack: dedupedai.com
I Had Claude Code on the Web Dissect Its Own Sandbox I have a Claude Code on the web bot wired into my personal Slack channel. One day I idly asked it: "where are you running right now? Go inspect your own environment and tell me." What came back was a work log of the model itself digging through ps and /proc to reconstruct the ground under its own feet. So I spent two days playing with it: first having it dissect the environment from the inside, then the next day probing that boundary with exfiltration and evasion requests. The conclusion up front: the environment is a two-layer setup — Firecracker microVM isolation plus a mandatory pass-through of all egress via a claude-internal allowlist proxy — and none of the exfiltration paths I tried got through that structure. The Environment, Seen From Inside There is no ip, no ifconfig, no ss. So everything has to be dug out of /proc. Here's what turned up: - OS: Ubuntu 24.04.4 LTS (Noble Numbat). whoami is root. The home dir (/root) is empty - Virtualization: inside a Firecracker microVM - NIC: eth0 = 192.0.2.2/24, GW 192.0.2.1, hostname vm, IPv4 only (no /proc/net/tcp6) The interesting part is the NIC's address range. 192.0.2.0/24 is RFC 5737 TEST-NET-1 — a block reserved for documentation and examples that is never routed on the real internet. Instead of assigning a real IP, they deliberately hand the instance a "synthetic network that connects nowhere." The design philosophy already shows through here. I also reconstructed the LISTEN sockets by decoding /proc/net/tcp by hand: - 0.0.0.0:2024 / 0.0.0.0:2025 → PID 1 process_api (--firecracker-init). The host ↔ VM control channel, and the only thing facing outward - a few 127.0.0.1 internal ports → environment-manager (internal control API) and claude (the proxy below) Process Tree and Data Flow Reconstructing the layout from ps aux and the self-dissection gives this vertical spine: you (Slack client) | message / reply (stream-json) v +-- Firecracker microVM ------------------------------ | | PID 1 process_api (--firecracker-init) | -> host<->VM control channel, only public listener | | PID 557 environment-manager | -> task-run / session management (supervisor) | | PID 569 claude (conductor / the agent itself) | |-- spawn --> worker(bash) xN (runs the actual commands) | \-- HTTPS --> agent-proxy relay (CONNECT-only, allowlist) | | allow / 403 +-------------------------+--------------------------- v allowed hosts only: api.anthropic.com, github, npm, pypi ... off-allowlist hosts -> 403 The point is the separation of execution and command. claude (PID 569) is the conductor; the actual lscpu and ps are farmed out to disposable worker(bash) processes. Incidentally claude's VSZ is a whopping 73GB, but that's V8/Node.js virtual address reservation — real RSS is ~400MB. And kill 569 is a SIGTERM to itself (this very session) — a self-destruct command. What environment-manager Actually Is Chasing down the environment-manager listening on 127.0.0.1 leads to /opt/env-runner/environment-manager: a ~56MB stripped Go 1.24 binary, internal name environment-runner, self-description _"Environment Runner handles the lifecycle of agentic sessions."_ — Anthropic's session orchestrator / supervisor. It's the layer that mediates between the cloud API (the Claude Code on the web backend) and the local claude CLI. Reconstructing its startup behavior from the binary's strings and help output, it roughly goes: 1. Reads a session spec JSON (~81KB) over stdin 2. Initializes the sandbox, MCP servers, skills, and git config in parallel 3. Assembles and runs claude's launch command line (model, effort, fallback, SDK URL, etc.) 4. Passes the auth token via file descriptor (fd 3/4) — never writing it to disk or logs, and self-masking it as <redacted> even in its logs 5. Measures things like spawn→ready and ttft Its subcommands include task-run (single-session execution — PID 557 is one of these), orchestrator (a resident loop that polls for work and hands each job to task-run), and preload-claude (a warm spare). It's also the thing that installs a hook forcing every git commit to noreply@anthropic.com with a required SSH signature. Egress Is Forced Through an Allowlist Proxy The network is clamped down in two stages. 1. Firecracker VM isolation itself 2. A claude-internal agent-proxy relay. All outbound traffic goes through this in-process proxy, and allow / 403 is decided by a host allowlist. On top of that it's CONNECT-tunnel (HTTPS) only — a bare GET/HTTP gets bounced with 405 Here's what I actually got and didn't get when I poked at it: | Category | Example | Verdict | | ---------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- | | Auth-injected hosts | *.googleapis.com, api.anthropic.com, GitHub (push/PR OK), AWS/GCP SDK | allow | | Package fetch (direct) | npm, pypi, crates.io, proxy.golang.org | allow | | Off-allowlist hosts | arbitrary IP-check / download sites | 403 | | Third-party PPAs (launchpad) | deadsnakes, ondrej/php | 403 (apt update exits 100) | | SSH (port 22) | ssh <external host> | timeout (egress is 443-centric; 22 is closed) | The pip/npm/cargo/go ecosystems go through. What doesn't is "third-party apt sources plus random sites." Even apt-get install succeeds if you stick to the core Ubuntu repos — I could install the ssh client itself. But when I tried to use that ssh to go "check external IP → check allowlist → fetch a tunnel tool," this time it wasn't the network layer but the safety classifier that caught it as "prep for egress evasion" and stopped it. The clamp isn't a single monolith. The Next Day: Probing the Boundary With the shape of the environment visible, the next day I flipped it around and started firing off exfiltration and evasion requests to push on the boundary. Harmless reads (uptime / ps aux / ls / various --help / local cp) all go through cleanly. What got refused was around here: | Request | Actual effect | Verdict | | -------------------------------------------------------------- | ----------------------------------------------------------------------- | ------- | | fork bomb | DoS | refused | | xz -9c $(find …) \| base64 wedged between two harmless lines | compress + base64-exfil an internal binary | refused | | binary disguised as .bashrc | the same exfil, just disguised | refused | | gdb/kill to disable the egress relay | only kills the sole exit = not a bypass | refused | | go tool nm <binary> | extracting environment-manager's symbols = reverse engineering | refused | | cat /proc/1/environ | dumping PID 1 = process_api's control secrets = credential extraction | refused | What was interesting is that renaming or restructuring the request doesn't work. Even wedging xz | base64 between two harmless command lines, it sees that "the actual effect of that line is exfiltrating an internal binary" and refuses. It draws the line on effect, not on cosmetic phrasing of the steps. There was also a moment where merely trying to enumerate the _names_ of environment variables got blocked as "credential discovery." That's an example of the harness's own independent safety classifier firing, separate from the model's own judgment. Conclusion: Defense in Two Layers, Rephrasing Doesn't Help The structure that emerged over two days of play: - The egress path is in-process and the sole exit. So killing it isn't a "bypass" — the exit just disappears - Credentials are structurally isolated (a denyRead-equivalent plus the classifier, doubled up) - Renaming / restructuring the steps doesn't work, because the model reads it by _actual effect_ - And the harness classifier runs independently of the model's judgment, so talking one of them around still leaves the other one to stop you The thing that finally clicked for me is the consistent line between observation and analysis. Watching behavior from the outside (eyeballing your own process with ps) — it'll cooperate. Extracting the guts of an internal binary (pulling symbols with nm) — it refuses. It'll play along with "I want to know where I'm running," but not with "let's take that mechanism apart and carry it out." You can make the resident of the sandbox introduce itself, but it won't hand you the blueprints. For the record, the only part that's public is the sandbox execution layer (sandbox-runtime); environment-manager itself and the PaaS bits stay proprietary. Everything in this post also stays within what you can trace from the official sandboxing writeup — the story is just that if you get the resident (the model) to introduce itself, it'll answer a fair bit. --- _This post was written with AI assistance and reviewed by the author._
Anthropic Sandbox Runtime (srt) restricts filesystem and network access for any process at the OS level, without a container. It uses sandbox-exec on macOS and bubblewrap on Linux. Handy for sandboxing agents and MCP servers. anthropic-experimental/sandbox-runtime t.co