shell-mcp
Scoped, allowlisted shell access for Claude Desktop and other MCP clients.
shell-mcp is a small Rust binary that speaks the Model Context Protocol
over stdio. It exposes two tools — shell_exec and shell_describe — and
enforces a strict, layered safety model so you can hand a Claude session
useful read access by default and opt in to write access per directory.
Why another shell server?
Most "shell" MCP servers either run anything the model asks (scary) or
require you to enumerate every command up front (tedious). shell-mcp takes
a middle path:
- A curated, platform-aware read-only allowlist is on by default
(
ls,git status,cargo metadata, etc.). - Write commands require an explicit
.shell-mcp.tomlin the project, with shell-style glob patterns (cargo build **). - Configuration files are discovered by walking up the directory tree
like git does, so a workspace can layer rules over a repo over a global
default in
~/.shell-mcp.toml. - A small hard denylist (
sudo,rm -rf /, fork bombs) is enforced before the allowlist and cannot be overridden. - All shell metacharacters (
; && || | $() backticks > < >>) are rejected. If you need a pipeline, write a script and allowlist the script.
Install
This drops a shell-mcp binary on your PATH.
Wire it into Claude Desktop
Edit your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json
on macOS; %APPDATA%\Claude\claude_desktop_config.json on Windows):
--root pins the launch root. Every command the model runs is forced to
execute inside that directory or one of its subdirectories. If you omit
--root, shell-mcp uses its own current working directory at launch.
Restart Claude Desktop and the shell_exec and shell_describe tools will
be available.
Tools
shell_describe
{ "cwd": "optional/relative/subdir" }
Returns the merged allowlist for the given subdirectory (or the launch root), the resolved working directory, the platform label, and the list of TOML files that were loaded in merge order. Call this first in every new session so the model can see what it's allowed to run.
shell_exec
{
"command": "git status --short",
"cwd": "optional/relative/subdir"
}
Returns:
{
"ok": true,
"cwd": "/abs/path/where/it/ran",
"matched_rule": "git status **",
"matched_rule_source": "/abs/path/.shell-mcp.toml",
"exit_code": 0,
"truncated": false,
"timed_out": false,
"stdout": "...",
"stderr": ""
}
If the command is rejected, ok: false and a rejection block names the
layer that refused it (metacharacter, hard_deny, escapes_root,
not_allowlisted).
Configuration
A .shell-mcp.toml file looks like this:
= true
= [
"cargo build",
"cargo build **",
"git commit -m **",
"./scripts/deploy.sh **",
]
Pattern syntax (one entry = one shell-tokenized pattern):
| Pattern | Matches |
|---|---|
git status |
exactly git status |
cargo build * |
cargo build plus exactly one more argument |
cargo build ** |
cargo build plus any number of arguments (incl. zero) |
cargo test foo?? |
cargo test foo plus any two characters |
** only acts as a rest-matcher when it's the final token.
Discovery and merging:
- Start at the working directory
shell-mcpis asked to run a command in. - Walk up to filesystem root collecting every
.shell-mcp.toml. - Prepend
~/.shell-mcp.tomlif present. - Merge outermost-first; the innermost file wins for
include_defaults, and rules from every file are concatenated.
The merge result is cached per (launch_root, cwd) pair.
Safety model in one paragraph
shell-mcp runs commands by spawning the program directly with discrete
arguments — no shell is invoked. Any input containing shell
metacharacters is rejected outright before parsing. Tokenized commands are
checked against a small hard denylist (sudo, rm -rf /, etc.) that no
user TOML can override. The working directory is normalized lexically and
forced to stay inside the launch root. Only after all of that does the
allowlist matcher decide whether the command runs. Output is captured
separately for stdout and stderr, normalized from CRLF, and clipped at
200 lines or 8 KB per stream with an explicit truncated flag.
Default allowlist
Unix (macOS + Linux):
ls, cat, head, tail, wc, grep, rg, find, tree, file,
stat, pwd, which, echo, env, git status|log|diff|show|branch,
git remote -v, cargo metadata|tree|--version, rustc --version.
Windows:
dir, type, findstr, where, tree /F, git status|log|diff|show|branch,
git remote -v, cargo metadata|tree|--version, rustc --version, whoami.
Building from source
Run the tests:
CI runs the full matrix on Ubuntu, macOS, and Windows on every push.
Status
v0.1.0. The MCP wire shape and the TOML schema are stable for the v0.1 series. Pipelines, environment variable controls, and per-rule timeouts are on the v0.2 roadmap.
License
MIT — see LICENSE.