Skip to main content
Glama

mcp-linux-sandbox

A disposable Debian Linux sandbox exposed as an MCP server, so a chat client (FlowDown, Claude Desktop, anything speaking Streamable HTTP) can run real shell commands — apt install, curl, ffmpeg, Python — and hand files back to you.

Each conversation gets its own container. Files under /workspace survive container recycling; installed packages and running processes do not.

MCP client ──Bearer──▶ nginx (TLS)
                         ├── /mcp/<random>/  ──▶ 127.0.0.1:8000  FastMCP
                         └── /dl/<signed>    ──▶ (same)  file links
                                                     │
                                                     ▼
                                    podman exec  mcpsb-<label>
                                                     │
                                        ┌────────────┴────────────┐
                                        ▼                         ▼
                              container writable layer     volume /workspace
                              (packages, /tmp — dies)      (files — survives)

Files

Path

What it is

server.py

The MCP server: run_command, present_file, sandbox_info, plus the /dl/ route

Dockerfile

Base image for sandbox containers (Debian + common CLI tools)

requirements.txt

Pinned Python dependencies

env.sh

Creates mcp-sandbox.env and generates its signing key

install.sh

Idempotent install/update: venv, image, systemd units

selftest.py

End-to-end checks that speak MCP over HTTP

scripts/cleanup.sh

Stops leftover containers on service stop

scripts/gc-volumes.sh

Deletes stale volumes (dry-run by default)

systemd/

mcp-sandbox.service, plus the volume-GC service and timer

nginx/mcp-sandbox.conf

The two location blocks to paste into your server

mcp-sandbox.env.example

Copy to mcp-sandbox.env; holds the signing key

Related MCP server: railway-sandbox-mcp

Install

sudo dnf install -y podman catatonit        # catatonit is needed by podman --init
./env.sh                                    # creates mcp-sandbox.env, generates the signing key
vim mcp-sandbox.env                         # set MCP_PUBLIC_BASE
./install.sh                                # refuses to run while that is still the placeholder

Then wire up nginx — copy both blocks from nginx/mcp-sandbox.conf into your existing TLS server {}, replacing the path and token placeholders:

  • generate the bearer token with openssl rand -hex 32

  • on Fedora, sudo setsebool -P httpd_can_network_connect 1, or nginx gets 502

  • sudo nginx -t && sudo systemctl reload nginx

Client configuration

Field

Value

Endpoint

https://your.host:8443/mcp/<random-path>/

Headers

{"Authorization": "Bearer <token>"}

Tool confirmation

leave it ON — this is a remote code-execution endpoint

Add this to the client's system prompt, or the per-conversation isolation does not happen:

When you call run_command, pass a sandbox label. At the start of a new conversation pick a NEW short label (for example amber-otter) and use that same label for every call in the conversation. Never reuse a label from an earlier conversation, and do not change it mid-conversation.

Verify with, from the install directory (install.sh copies selftest.py there):

cd ~/mcp-linux-sandbox                      # or wherever SANDBOX_HOME points
./venv/bin/python selftest.py --url https://your.host:8443/mcp/<path>/ --token <token>
./venv/bin/python selftest.py --url http://127.0.0.1:8000/mcp    # bypasses nginx

Sandbox labels

Tools take a sandbox label, and label → container → volume is one to one: a label with no container yet gets a fresh, empty one. Labels are normalised, so Amber Otter and amber-otter reach the same sandbox.

The label has to come from the model because the client sends one MCP session id for the whole app rather than one per conversation, and no per-conversation field exists in the request. It is a convention rather than a guarantee — a model that reuses a label reaches the old container — but every result echoes [sandbox <key>] [new], so a collision is visible rather than silent.

Settings

Setting

Default

Effect

SANDBOX_IDLE

300 s

Recycle idle containers. Cleanup only: lowering it loses apt packages sooner, nothing else.

SANDBOX_MAX

2

Concurrent containers. Beyond it the least-recently-used is evicted (rm -f; the volume stays, so files survive). On a 4-core / 3.66 GiB host, 2 × 1 GiB leaves ~1.6 GiB for nginx, the server and the system.

SANDBOX_MAX_TIMEOUT

900 s

Ceiling on the model's timeout_seconds.

SANDBOX_MIN_FREE

2 GiB

Refuse to start a sandbox when the volume filesystem is nearly full. Checked at container creation only — it will not stop a single large write.

SANDBOX_LINK_TTL

3600 s

Download links are HMAC-signed with an expiry, so a leaked URL is bounded in time and scope.

SANDBOX_IMAGE_MAX / SANDBOX_INLINE_MAX

1.5 MiB / 48 KiB

Attached images are also sent to the model (~2 MB ≈ 20k tokens); oversized files fall back to a link.

--memory=1g --cpus=2 --pids-limit=256

Verified to land in the cgroup (memory.max, cpu.max, pids.max).

--init

Without an init as PID 1, exited children pile up as zombies against --pids-limit until exec starts failing.

bash -lc

/bin/sh is dash on Debian: no [[ ]], no arrays, no pipefail.

--security-opt=no-new-privileges

Keeps setuid binaries from being exploitable.

Volumes are never deleted by the server. gc-volumes.sh handles that on a timer and is dry-run by default.

Security

This endpoint executes arbitrary commands. Treat it accordingly.

  • Keep the client's tool-confirmation prompt enabled. It is the only human gate on what actually runs.

  • Anything that can reach /mcp/ can run code; anything that can reach /dl/ can read the sandbox volume. Keep the bearer token and MCP_SIGN_KEY out of URLs, screenshots, .fdmcp-style exports, cloud drives and git.

  • present_file serves regular files under /workspace and follows no symlink in any path component, so a link created inside the sandbox cannot reach host files.

  • Containers are rootless (podman, no sudo) and can reach the LAN by default. If that matters, drop the network (--network=none) or move the container onto an --internal network behind a proxy — but installing packages needs network, so pick one.

  • Isolation is namespace + seccomp + cgroups, not a VM. It is the right level for "stop the model from trashing my host"; it is not gVisor/Kata.

Limitations

  • The sandbox boundary is the model's own label discipline, as described above.

  • Concurrent conversations share the container budget. With SANDBOX_MAX=2, a third conversation evicts the LRU container — packages go, files stay.

  • Restarting the service invalidates the client's MCP session; the client has to re-verify.

  • Image attachments depend on the client. FlowDown attaches MCP image content; other clients may not.

  • gc-volumes.sh's age heuristic (newest mtime in the volume) has not been validated on btrfs. Keep DRY_RUN=1 until you have watched it for a while.

Verified behaviour

Checked end-to-end against a live deployment (Raspberry Pi, Fedora 44 aarch64, rootless podman, cgroup v2, behind nginx):

Check

Result

Debian userland, tools present

✅ trixie; curl jq git rg fdfind ps less nano python3

Network + apt install

sqlite3 installed and usable

Same label reuses container

✅ same hostname, no [new]

New label, fresh container

✅ different hostname, empty /workspace

Files survive recycling

✅ marker file kept its old hostname after rebuild

Writable layer is discarded

/tmp contents gone after rebuild

Idle reaper

✅ containers recycled after the idle window

LRU eviction at MAX_SANDBOXES

✅ count stayed 2/2, oldest evicted, its files kept

Exit codes

false→1, exit 42→42, unknown command→127

Container-side timeout

✅ rc=124, process actually killed

--init as PID 1

podman-init, 0 zombies

cgroup limits applied

memory.max=1G, cpu.max=2 cores, pids.max=256

present_file image

✅ rendered as an attachment

present_file text / link

✅ inlined / signed URL

Path traversal

✅ rejected

Symlink escape

✅ rejected

Oversized image

✅ falls back to a link

Link expiry and signature tampering

✅ expired and tampered URLs rejected

Credential redaction in sandbox_info

<redacted>

Not yet exercised: the over-age branch of gc-volumes.sh.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to execute Python, JavaScript, Bash, and Go code in blazing-fast (~0.1ms startup), isolated cloud containers with secure, ephemeral environments that auto-destroy after use.
    156
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables ChatGPT to create, manage, and execute commands in ephemeral isolated Linux sandboxes on Railway, with file operations and checkpointing.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to work in a persistent, isolated Linux workspace with file management, Bash execution, SSH/SFTP access, and durable storage while keeping workloads contained from the host and private networks.
    MIT