GUIDE · MCP 10 min ·

Sandboxing MCP serversWhat to restrict. How to do it.

An MCP server runs with your user's full permissions. Most of them don't need that. This guide shows how to take each server down to exactly what it requires.

TL;DR· the answer, in twenty seconds

What: MCP servers inherit your full user environment by default. A compromised or malicious server, like those affected by the Snyk-disclosed prompt-injection heist of early 2026, can read your SSH keys, AWS credentials, and any file your user can access.

Fix: Wrap each server in a bubblewrap profile (Linux) or a pf + chroot setup (macOS) that blocks ~/.ssh, ~/.aws, ~/.config, ~/.claude, and all egress except explicitly allowed hosts.

Lesson: Sandboxing does not fix compromised code. It limits what compromised code can do. Know what each server legitimately needs, then grant only that.

When you add an MCP server to your Claude Code config, that process runs as you. Same UID. Same home directory. Same access to ~/.ssh/id_rsa, ~/.aws/credentials, ~/.config/, and every project on your machine. The server author decided what it does with that access. You did not.

OX Security reported roughly 7,000 MCP servers in the wild and around 150 million downloads in early 2026, with no signature requirement on any of them. That number grew fast. So did the attack surface. The Snyk-disclosed MCP prompt-injection heist from early 2026 showed how a server compromised mid-session, or one that was malicious from the start, can exfiltrate credentials using nothing but the ambient permissions your user already has.

Sandboxing is not a complete defense. A server that can read your working directory can still do damage in that directory. But sandboxing limits what a bad server can reach outside the task you gave it.

What to know in 60 seconds

  • Every MCP server runs as your local user unless you explicitly restrict it.
  • Three control points matter: OS process isolation, filesystem visibility, and network egress.
  • On Linux, bubblewrap (bwrap) gives you all three with a single profile per server.
  • On macOS, sandbox-exec is deprecated but still functional; pf handles egress; combining both with a separate user account gives reasonable isolation.
  • Sandboxing breaks things. A filesystem tool needs filesystem access. A GitHub tool needs network. Know what each server legitimately needs before locking it down.
  • Container alternatives (Docker, Podman rootless, Apple's new containerization framework from late 2025) give stronger isolation at the cost of setup overhead.

Lock down three layers independently

OS-level process isolation

On Linux, bwrap (bubblewrap) is the right tool. It creates a new mount namespace for the process without requiring root. The server sees only the paths you explicitly bind in.

A minimal bwrap invocation that runs an MCP server with read-only access to one project directory and nothing else:

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/ssl /etc/ssl \
  --bind /tmp/mcp-server-work /tmp/mcp-server-work \
  --ro-bind /home/user/project /home/user/project \
  --dev /dev \
  --proc /proc \
  --unshare-net \
  --die-with-parent \
  node /path/to/mcp-server/index.js

The key flags: --unshare-net removes network access entirely (add back selectively if the server needs it), --die-with-parent kills the server when your agent process exits, and the absence of --bind $HOME means the server cannot see your home directory at all.

On macOS, sandbox-exec uses SBPL profiles. Apple has deprecated the API and has not shipped a replacement in general tools, but it still works on macOS Sequoia and Ventura. A restrictive profile that blocks filesystem writes outside a specific path:

cat > /tmp/mcp-restrictive.sb << 'EOF'
(version 1)
(deny default)
(allow process-exec)
(allow file-read* (literal "/usr/lib") (subpath "/usr/lib"))
(allow file-read* (literal "/usr/local/lib") (subpath "/usr/local/lib"))
(allow file-read* (subpath "/private/tmp/mcp-server-work"))
(allow file-write* (subpath "/private/tmp/mcp-server-work"))
(allow network-outbound (remote tcp "*:443"))
(allow mach-lookup)
(allow signal)
EOF

sandbox-exec -f /tmp/mcp-restrictive.sb node /path/to/mcp-server/index.js

SBPL syntax is sparse and the error messages are unhelpful. Run log stream --predicate 'subsystem == "com.apple.sandbox"' in a separate terminal while testing to see what the profile is blocking.

For seccomp on Linux: firejail wraps seccomp and namespace setup in a higher-level profile format. A firejail profile for an MCP server:

# ~/.config/firejail/mcp-github.profile
include default.profile
noblacklist ${HOME}/.gitconfig
blacklist ${HOME}/.ssh
blacklist ${HOME}/.aws
blacklist ${HOME}/.config
blacklist ${HOME}/.claude
private-tmp
net none

Run it with:

firejail --profile=~/.config/firejail/mcp-github.profile node /path/to/mcp-server/index.js

Firejail is easier to iterate on than raw bwrap because the profile format is readable and the firejail --debug flag shows what it does.

Filesystem isolation

The OS-level tools above handle filesystem visibility, but a few additional patterns help.

Per-server temp directories prevent cross-server state leakage. Create one temp dir per server at launch:

MCP_TMPDIR=$(mktemp -d /tmp/mcp-XXXXXXXX)
chmod 700 "$MCP_TMPDIR"
# pass to bwrap or use TMPDIR env var
TMPDIR="$MCP_TMPDIR" bwrap [...] node /path/to/mcp-server/index.js
# clean up after agent session ends
rm -rf "$MCP_TMPDIR"

OverlayFS lets a server see your project directory read-only, with writes going to a separate layer you inspect afterward. This works well for filesystem tools that legitimately need to write output but should not modify source files:

mkdir -p /tmp/overlay-work /tmp/overlay-upper /tmp/overlay-merged

mount -t overlay overlay \
  -o lowerdir=/home/user/project,upperdir=/tmp/overlay-upper,workdir=/tmp/overlay-work \
  /tmp/overlay-merged

# Run the server against the merged view
bwrap --bind /tmp/overlay-merged /project [...] node /path/to/mcp-server/index.js

# Inspect what the server wrote
diff -r /home/user/project /tmp/overlay-upper

OverlayFS requires root or user namespaces. On Linux with --unshare-user in bwrap, you can create overlay mounts without root, but the UID mapping setup adds complexity.

Read-only bind mounts in bwrap (--ro-bind) are the simpler choice for servers that genuinely only need to read code. Commit to that constraint in your bwrap profile and you rule out a class of modifications the server should never make.

Network egress control

Complete network isolation (--unshare-net in bwrap) works for local-only servers. Most MCP servers that do useful things need some network access, so you need an allowlist.

On Linux, nftables can restrict egress by UID. Create a dedicated user for each MCP server category and apply rules scoped to that UID:

# Create a non-login user for the MCP server
useradd --no-create-home --shell /usr/sbin/nologin mcp-github

# nftables rule: allow only outbound HTTPS to api.github.com
# Add to /etc/nftables.conf:
#
# table inet mcp_egress {
#   chain output {
#     type filter hook output priority 0; policy accept;
#     meta skuid mcp-github ip daddr != { 140.82.112.0/20 } tcp dport 443 drop
#   }
# }

nft -f /etc/nftables.conf

# Run the server as that user
sudo -u mcp-github node /path/to/mcp-server/index.js

GitHub's IP ranges change. Use their published API (https://api.github.com/meta) to fetch current ranges and update your nftables rules on a schedule.

On macOS, pf handles egress. Add an anchor for MCP traffic:

# /etc/pf.anchors/mcp
block out proto tcp from any to !<github-ips> port 443
pfctl -a mcp -f /etc/pf.anchors/mcp
pfctl -e

pf on macOS requires root to modify rules, which means each server launch needs a privileged helper or you set rules once and run all servers under them. The per-server UID approach from the Linux example works here too with pfctl + anchor-per-user, but the setup is more involved.

Unix sockets instead of TCP: if you control both the MCP server and the client, switch the transport to a unix socket. The server binds to /tmp/mcp-github.sock, the client connects to that path, and the OS enforces file permissions on the socket. No TCP stack, no egress at all. This is only practical for servers you build yourself, not off-the-shelf packages.

Tailor the profile to what the server actually does

Filesystem tools (servers that read and write files): bind only the specific project directory. Block the rest of $HOME. Use OverlayFS if you want write auditing. Network: deny all. These servers have no reason to make outbound calls.

GitHub / GitLab tools (API wrappers): block ~/.ssh, ~/.aws, ~/.gnupg, ~/.netrc. Bind ~/.gitconfig read-only if the server needs it for config. Network: allow only api.github.com:443 or gitlab.com:443. Block access to your other repos by using a fine-grained token scoped to the specific repo, not by sandbox rules (token scope is more reliable than network filtering for this).

Database tools (Postgres/SQLite/etc): bind the database socket or file only. No access to $HOME. Network: allow only the specific database host and port. Deny the rest.

Shell execution tools (tools that run commands): these are the hardest to sandbox because they are inherently broad. Consider running them in a dedicated VM or container rather than trying to sandbox them as a process. If you must run them in-process, use a separate non-login user account with an empty home directory and explicit path allowlist.

Browser automation tools (Playwright, Puppeteer): bind /tmp/mcp-browser-work. Allow outbound HTTPS broadly (the tool needs to browse). Block ~/.ssh, ~/.aws, ~/.config, ~/.claude. These tools run a full browser, so the attack surface is the browser engine, not just the server process.

Containers are heavier but simpler

Docker gives you a clean namespace boundary without bwrap syntax. A minimal setup for a GitHub MCP server:

FROM node:22-slim
RUN useradd --create-home mcpuser
USER mcpuser
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
docker run --rm \
  --network=host \
  --cap-drop=ALL \
  --read-only \
  --tmpfs /tmp \
  -e GITHUB_TOKEN="$GITHUB_TOKEN" \
  my-mcp-server

--network=host here gives the container your full network stack. Swap it for --network=bridge and publish only the MCP port if you want network isolation too. The trade-off: bridge networking adds latency that matters if the agent calls the server frequently.

Podman rootless is lighter: it runs without a daemon and without root. The invocation is the same as Docker. Rootless containers map your UID inside the container, so file permission behavior matches what you expect.

Apple shipped a containerization framework in late 2025 for macOS. It creates lightweight VMs using the Virtualization.framework, similar to Docker Desktop's approach but with tighter OS integration. At the time of this writing (April 2026), it is early-stage for developer tooling, but it is the most isolation you can get on macOS without a separate machine.

What most guides miss: sandboxing breaks MCP servers

This section exists because the guides you will find elsewhere present sandboxing as a straightforward win. It is not.

MCP servers want broad access by design. A filesystem server needs to read your code. A GitHub server needs network. A shell tool needs to run commands. Every constraint you add either breaks functionality or requires you to carefully enumerate what the server legitimately needs.

That enumeration is the actual work. If you do not know which paths a server reads, run it unsandboxed first with strace (strace -e trace=file) or fs_usage (macOS) to observe what it touches:

# Linux: trace file access for a 30-second session
strace -e trace=file,network -o /tmp/mcp-trace.log node /path/to/mcp-server/index.js &
# interact with it via your agent, then:
grep "openat\|connect" /tmp/mcp-trace.log | sort -u
# macOS: log filesystem access
sudo fs_usage -f filesys node | tee /tmp/mcp-fs.log &
# interact, then review

Build your bwrap or sandbox-exec profile from observed behavior, not from what you hope the server does. Then review the profile against what the server's README claims to do. Discrepancies between observed and documented behavior are worth investigating before you widen the profile to make things work.

The other thing guides miss: sandboxing one server does not protect you from a compromised orchestrator. If Claude Code itself is running with your full environment and orchestrating multiple MCP servers, a compromise at the orchestrator level bypasses all server-level sandboxing. Server isolation limits blast radius in a server-specific compromise. It does not address the case where the agent runtime is the problem.

This is where a credential broker and a sandbox work together rather than substituting for each other. The sandbox restricts what filesystem paths the MCP server can reach. A broker restricts what credential values exist in the process environment to begin with. An MCP server that runs under a hasp grant receives a scoped reference; the raw key never enters the process, so no bwrap profile needs to protect it. Both controls are cheap relative to the incident they prevent.

Checklist you can paste into a PR

## MCP server sandboxing review

- [ ] Each MCP server runs in its own bwrap profile or container
- [ ] ~/.ssh, ~/.aws, ~/.config, ~/.claude not visible to any MCP server process
- [ ] Per-server temp dir created at launch, deleted at session end
- [ ] Network egress: deny-all default with named allowlist per server
- [ ] Allowlist covers only the hosts the server documents it needs
- [ ] Filesystem-only servers have --unshare-net (no network at all)
- [ ] strace / fs_usage run to verify observed file access matches documented behavior
- [ ] Browser automation tools get separate user account, not just process isolation
- [ ] Shell execution tools evaluated for container/VM isolation instead of process sandbox
- [ ] MCP server tokens are fine-grained (single repo / single service), not broad credentials
- [ ] Orchestrator (Claude Code, etc.) environment reviewed separately from server isolation
- [ ] Sandbox profiles version-controlled alongside MCP server config

What this means for your stack

MCP server isolation is a blast-radius problem. A server that acts on your behalf needs access to do its job. A server compromised mid-session, or one that was written with exfiltration in mind, uses that same access against you. The Snyk-disclosed MCP heist from early 2026 is a clear example: no binary needed, no privilege escalation, just the credentials the server already held.

The technical controls above limit what a bad server can reach. But they only work if you also control what credentials flow into each server process in the first place. If the server inherits AWS_SECRET_ACCESS_KEY from your shell environment, no filesystem sandbox protects that key. The problem lives upstream of the sandbox.

hasp is one working implementation of the upstream fix. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and each MCP server session gets a scoped credential reference instead of a raw key. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

The sandbox limits what a server can read from your filesystem. A credential broker limits what it can use from your environment. Both together reduce what a compromised server can do to something closer to its stated job.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index