shell-mcp
A super-secure MCP (Model Context Protocol) server for running shell commands safely with a local LLM client.
Security Model
Commands pass through 8 independent security layers — every one must pass:
| Layer | What it does |
|---|---|
| 1 | Zod schema validation — strict type checks before any processing |
| 2 | No-shell spawn — execFile with shell: false, no /bin/sh -c "..." ever |
| 3 | Hardcoded blocklist — rm, sudo, dd, curl, brew, docker, etc. permanently blocked |
| 4 | Argument inspection — rejects metacharacters, null bytes, suspicious patterns, per-command dangerous flags |
| 5 | Directory allowlist — every path must be inside --allow-dir |
| 6 | Symlink resolution — symlinks resolved before path check (no escape via symlink) |
| 7 | Resource limits — timeout, output size cap, concurrency limit |
| 8 | Sanitized environment — strips AWS_*, *_TOKEN, *_SECRET, all cloud provider secrets |
Quick Start
# Clone and build
git clone https://github.com/your-username/shell-mcp.git
cd shell-mcp
npm install
npm run build
# Run (must specify at least one --allow-dir)
node dist/index.js --allow-dir ~/projects --allow-dir ~/data
LLM Client Configuration
{
"shell-mcp": {
"command": "node",
"args": [
"/path/to/shell-mcp/dist/index.js",
"--allow-dir", "/home/user/projects",
"--allow-dir", "/home/user/data",
"--allow-cmd", "ls",
"--allow-cmd", "cat",
"--allow-cmd", "grep",
"--allow-cmd", "find",
"--allow-cmd", "python3",
"--timeout", "30",
"--log-file", "/home/user/.shell-mcp/audit.log"
]
}
}
Flags
| Flag | Description | Default |
|---|---|---|
--allow-dir <path> |
Permitted directory (required, repeatable) | none — server won't start |
--allow-cmd <cmd> |
Whitelist specific commands (repeatable) | all non-blocked |
--block-cmd <cmd> |
Extra commands to block | — |
--timeout <secs> |
Max execution time (1–300) | 30 |
--max-output <bytes> |
Max output size | 1048576 (1MB) |
--max-concurrent <n> |
Max parallel commands (1–10) | 3 |
--log-file <path> |
Audit log file path | stderr only |
--enable-write |
Enable write_file tool |
off |
--allow-overwrite |
Permit overwriting files | off |
--read-only-mode |
Disables all writes (overrides --enable-write) |
off |
--allow-network-cmds |
Unblock curl, wget, nc | off |
--allow-env-passthrough |
Forward full env to child processes | off (sanitized) |
Tools Exposed to the LLM
run_command
Run a command with explicit arguments array. No shell — pipes and redirects won't work.
{
"command": "grep",
"args": ["-r", "TODO", "/home/user/projects/myapp"],
"cwd": "/home/user/projects/myapp",
"timeout": 10
}
read_file
Read a file's contents. Supports line ranges and binary (base64) output.
{
"path": "/home/user/projects/myapp/src/main.py",
"startLine": 1,
"endLine": 50
}
list_directory
List a directory with tree view, sizes, and permissions.
{
"path": "/home/user/projects/myapp",
"recursive": true
}
write_file (requires --enable-write)
Write a file. Won't overwrite without --allow-overwrite.
{
"path": "/home/user/projects/myapp/notes.txt",
"content": "Hello world"
}
get_server_info
Returns current server config and restrictions. Useful for the LLM to understand its sandbox before acting.
Permanently Blocked Commands
These cannot be enabled by any flag:
rm, rmdir, shred, dd, mkfs, fdisk, diskutil, sudo, su, doas,
osascript, defaults, open, launchctl, crontab,
curl, wget, nc, ssh, scp, rsync,
brew, npm, yarn, pip, gem, cargo, docker,
gcc, clang, kill, killall, shutdown, reboot,
gpg, security (macOS keychain), ...and more
Audit Log
Every command attempt (allowed or denied) is logged in JSON:
{"timestamp":"2024-01-01T12:00:00.000Z","sessionId":"A3F9B2C1","event":"COMMAND_DENY","command":"rm","args":["-rf","/"],"denyLayer":3,"denyReason":"Command \"rm\" is on the hardcoded blocklist"}
{"timestamp":"2024-01-01T12:00:01.000Z","sessionId":"A3F9B2C1","event":"COMMAND_ALLOW","command":"ls","args":["-la"],"cwd":"/home/user/projects","exitCode":0,"durationMs":12,"outputBytes":1024}
Inspect with MCP Inspector
npx @modelcontextprotocol/inspector node dist/index.js --allow-dir /tmp/test