Mcp Stdio Shellguard — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Mcp Stdio Shellguard (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<!-- studiomeyer-mcp-stack-banner:start -->
Part of the [StudioMeyer MCP Stack](https://studiomeyer.io) — Built in Mallorca 🌴 · ⭐ if you use it
<!-- studiomeyer-mcp-stack-banner:end -->
<!-- badges -->
<!-- /badges -->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).
@modelcontextprotocol/sdk ^1.29.0npm install mcp-stdio-shellguardOr run the audit CLI directly without installing:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./srcThree layers, opt-in piecewise:
guardExec / guardSpawn you call from yourown MCP server. Default-deny allowlist, sandbox profiles, replay window.
mcp-shellguard-audit scan <path> walks the AST, reports12 anti-patterns from LOW (no timeout) to CRITICAL (exec(\...${userInput}...\)).
mcp-stdio-shellguard-demo exposes 8 toolsso the MCP Inspector / Claude Desktop can drive the bundle directly.
| 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. |
| 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.
| 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).
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-256mcp-shellguard-audit scan ./src
mcp-shellguard-audit scan ./src --format sarif --output audit.sarif
mcp-shellguard-audit scan ./src --severity-floor HIGH # CI gateExit codes:
0 clean (no findings at-or-above floor)1 findings present2 parse / IO errors| 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).
// shellguard:ignore-next-line — suppress one finding// shellguard:ignore-file — suppress whole file (rare; prefer per-line)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.
HOOK_RECIPES.md — Claude Code hook recipes that auto-block dangerous tool callsCHANGELOG.md — release historyMIT — Copyright (c) 2026 Matthias Meyer (StudioMeyer)
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.