About Stdio Shellguard
Stdio Shellguard is a TypeScript security bundle for developers building MCP servers that use stdio. It combines guarded process execution, source auditing, and a reference MCP server exposing the protections as tools.
Key features
- 01Allowlisted guarded exec and spawn
- 02Sandbox profiles and replay detection
- 03AST-based shell-call auditing CLI
- 04Scans for 12 unsafe patterns
- 05Reference server with eight tools
Use cases
- 01Guard shell execution in an MCP server
- 02Audit TypeScript or JavaScript for unsafe calls
- 03Generate SARIF audit output for CI
MCP support
The reference MCP server exposes eight tools for guarded execution, source auditing, replay checks, sandbox status, and trust tiers.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
Tools
8Saved tool list · updated 3w ago
| Parameter | Type | Required | Description |
|---|---|---|---|
| cwd | string | No | |
| env | object | No | |
| args | array | Yes | |
| command | string | Yes | |
| toolName | string | Yes | |
| timeoutMs | number | No | |
| blockOnReplay | boolean | No |
Details
From the project READMEPart of the StudioMeyer MCP Stack — Built in Mallorca 🌴 · ⭐ if you use it
mcp-stdio-shellguard
Defense-in-depth bundle for MCP stdio servers. Wraps `child_process.exec/spawn`with allowlist + sandbox + replay-detection, plus an AST audit CLI (mcp-shellguard-audit)
that scans MCP server sources for unsanitized shell calls. Closes the Ox-Security
MCP stdio-RCE class (200k vulnerable servers, May 2026 disclosure).
- MCP spec: 2025-06-18
- SDK:
@modelcontextprotocol/sdk^1.29.0 - Node: >= 20
- License: MIT
- Author: Matthias Meyer (StudioMeyer)
Install
npm install mcp-stdio-shellguard
Or run the audit CLI directly without installing:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
What it gives you
Three layers, opt-in piecewise:
- Library API — drop-in
guardExec/guardSpawnyou call from your own MCP server. Default-deny allowlist, sandbox profiles, replay window. - Audit CLI —
mcp-shellguard-audit scan <path>walks the AST, reports 12 anti-patterns from LOW (no timeout) to CRITICAL (exec(\...${userInput}...`)`). - Reference MCP server —
mcp-stdio-shellguard-demoexposes 8 tools so the MCP Inspector / Claude Desktop can drive the bundle directly.
Tools (reference server)
| Tool | Type | Purpose |
|---|---|---|
guard_exec | destructive | Defended child_process.exec. Forces args[] vector, allowlist + sandbox + replay. Returns stdout, stderr, exitCode, canonicalHash, isReplay, trustTier. |
guard_spawn | destructive | Defended child_process.spawn. Returns SHA-256 hashes of stdout/stderr instead of full bodies. Hard-rejects shell:true. |
register_allowlist | mutating | Register a tool name with executable + args regex. Without registration the default-deny applies. |
audit_source | read-only | Scan a TS/JS path for shell-injection anti-patterns. Returns AuditFinding[] + summary. |
audit_report | read-only | Format an audit result as markdown / json / SARIF 2.1.0. |
replay_check | read-only | Compute canonical SHA-256 hash for an invocation and report whether it's already in the replay window. |
sandbox_status | read-only | Report active sandbox profile + concrete limits + cgroup-v2 active flag. |
trust_tier | read-only | Derive LOW/MEDIUM/HIGH/CRITICAL tier for a registered tool plus improvement hints. |
Sandbox profiles
| Profile | Timeout | Max stdout | Max stderr | FD budget | cgroup-v2 |
|---|---|---|---|---|---|
strict | 5 s | 1 MB | 256 KB | 32 | yes (cpu/memory) |
standard (default) | 30 s | 10 MB | 1 MB | 256 | yes |
permissive | 5 min | 100 MB | 10 MB | 1024 | no |
Caller can tighten via timeoutMs / fdBudget per call. Caller cannot widen
beyond the profile.
Trust tiers
| Tier | Condition |
|---|---|
| LOW | tool not registered (default-deny) |
| MEDIUM | registered but argsPatterns empty (any args allowed) |
| HIGH | argsPatterns set but sandbox or replay tracker inactive |
| CRITICAL | argsPatterns + sandbox + replay all active |
Lift LOW → CRITICAL by registering the tool + setting argsPatterns + running
through guardExec/guardSpawn (which always activate sandbox + replay).
Library quickstart
import {
AllowlistRegistry,
ReplayWindow,
guardExec,
} from "mcp-stdio-shellguard";
const registry = new AllowlistRegistry();
const replay = new ReplayWindow();
registry.register({
toolName: "git-log",
executable: "/usr/bin/git",
argsPatterns: ["^log$", "^--oneline$", "^-n$", "^\\d+$"],
sandboxProfile: "strict",
});
const result = await guardExec(
{
toolName: "git-log",
command: "/usr/bin/git",
args: ["log", "--oneline", "-n", "10"],
},
{ registry, replay },
);
console.log(result.stdout); // → commit lines
console.log(result.trustTier); // → "CRITICAL"
console.log(result.canonicalHash); // → 64-char SHA-256
Audit CLI
mcp-shellguard-audit scan ./src
mcp-shellguard-audit scan ./src --format sarif --output audit.sarif
mcp-shellguard-audit scan ./src --severity-floor HIGH # CI gate
Exit codes:
0clean (no findings at-or-above floor)1findings present2parse / IO errors
Custom ignoreGlobs passed to scanPath or the audit_source MCP tool accept
at most 128 strings. Each string is limited to 1024 UTF-16 code units and 16
unescaped opening grouping characters ((, {, [). Inputs above these
budgets raise a controlled error before matching.
Anti-pattern library (12 rules)
| ID | Severity | Triggers on |
|---|---|---|
exec_template_literal_with_input | CRITICAL | child_process.exec(\ls ${x}`)` |
exec_dynamic_string | CRITICAL | child_process.exec(cmd) |
exec_sync_dynamic_string | CRITICAL | child_process.execSync(cmd) |
eval_near_child_process | CRITICAL | eval(...) |
function_constructor_near_child_process | CRITICAL | new Function(...) |
spawn_dynamic_file_args | HIGH | spawn(bin, userArgs) |
exec_file_dynamic | HIGH | execFile(bin, ...) |
shell_true_option | HIGH | { shell: true } |
os_system_equivalent | HIGH | Deno.run / Bun.spawn |
spawn_literal_dynamic_args | MEDIUM | spawn('git', userArgs) |
unbounded_buffer | LOW | exec without maxBuffer |
missing_timeout | LOW | exec/spawn without timeout |
The scanner resolves renamed child_process bindings before matching,
so the dangerous shapes below are caught even when the call goes through an
alias rather than a literal child_process.exec:
const execAsync = promisify(exec); execAsync(...${x})import cp from "node:child_process"; cp.exec(...${x})const { exec: sh } = require("child_process"); sh(...${x})import { exec as run } from "node:child_process"; run(...)
Synchronous variants (spawnSync, execFileSync) share their async rules,
and shell_true_option also fires on a string shell ({ shell: "/bin/sh" })
or a dynamic shell value — not just the literal { shell: true }. A
promisify of a non-child_process function, a destructure off another
module, and { shell: false } stay clean (no false positives).
Pragmas
// shellguard:ignore-next-line— suppress one finding// shellguard:ignore-file— suppress whole file (rare; prefer per-line)
Why this exists
Ox-Security disclosed (2026-05) that 200k+ MCP stdio servers wrap
child_process.exec with template literals carrying user input straight from
LLM tool args. LiteLLM v1.83.6 was the canonical example (CVE patched in 1.83.7).
This bundle is the defensive-security counterpart: a drop-in guard + scanner
that closes the class. Inspired by AWS Linux seccomp + Chromium sandbox tiers.
See also
HOOK_RECIPES.md— Claude Code hook recipes that auto-block dangerous tool callsCHANGELOG.md— release history- Ox-Security MCP audit: https://venturebeat.com/security/200000-mcp-stdio-servers/
- LiteLLM CVE-2026-XXXX: https://github.com/BerriAI/litellm/security/advisories
License
MIT — Copyright (c) 2026 Matthias Meyer (StudioMeyer)
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.