Skip to content

feat!: replace secure-mode shell_exec with typed tools - #31

Merged
sonirico merged 12 commits into
masterfrom
plan/mcp-shell-0.9.0-typed-tools
Sep 4, 2026
Merged

feat!: replace secure-mode shell_exec with typed tools#31
sonirico merged 12 commits into
masterfrom
plan/mcp-shell-0.9.0-typed-tools

Conversation

@sonirico

@sonirico sonirico commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Original motivation

1. 🎯 Desired functionality: secure mode exposes typed MCP tools whose argv is built by the server.
├── 1.1. Client parameters are values only and never land in a flag position.
├── 1.2. Example: git_log with {"max_count": 5, "path": "README.md"} runs git log --no-color -n5 --format=... --end-of-options HEAD -- README.md.
└── 1.3. Every path is resolved with symlinks and must stay under working_directory.

2. 🔁 Best current alternative: 0.8.x shell_exec with unfurling plus per-executable deny/allow argument policies.
├── 2.1. Seven advisories since May (latest GHSA-xvpv-9qcq-636h, --cont abbreviating --contents) came from that model.
└── 2.2. Git accepts any unambiguous long-option prefix, so a flag denylist cannot be completed.

Existing limitation

1. 🧱 Secure mode validated free text: unfurl.go parsed a shell AST and argpolicy.go filtered flags per executable.
├── 1.1. Any flag spelling git accepts but the policy does not know is a bypass.
└── 1.2. No path confinement existed: cat /etc/hostname was allowed by the default config.

Implementation strategy

1. ⚠️ Backwards compatibility: breaking, hence feat! and 1.0.0.
├── 1.1. shell_exec exists only with MCP_SHELL_ALLOW_UNSAFE=1 and is documented as unrestricted by design.
└── 1.2. Yaml keys use_shell_execution, allowed_executables, allowed_commands, blocked_commands, blocked_patterns fail config loading with a migration message.

2. 🔒 Make the injection class unrepresentable.
├── 2.1. workspace.resolve confines every path.
├── 2.2. Git paths go after --, refs after --end-of-options, refs starting with - are rejected.
└── 2.3. Existing git hardening kept and extended with core.hooksPath=/dev/null.

3. 🚪 Gates: write tools behind writes_enabled, run_script only for operator-defined scripts.

Code added

1. paths.go: workspace root resolution and path confinement.

2. tools_fs.go: read_file, list_dir, glob, grep, stat, diff_files, system_info, plus write_file, edit_file, mkdir, move, delete behind writes_enabled.

3. tools_git.go: git_status, git_log, git_diff, git_show, git_blame, git_branches, git_tags, git_rev_parse, git_ls_files, git_stash_list, git_remotes, plus git_add, git_commit, git_switch, git_restore, git_stash behind writes_enabled.

4. tools_script.go: run_script with argv taken verbatim from config.

Code changed

1. executor.go: split into run(argv) and runShell(command), unfurler removed.

2. config.go: removed keys rejected, writes_enabled and scripts added.

3. main.go: registerTools picks typed tools or shell_exec by mode.

4. Deleted: unfurl.go, argpolicy.go, security.go, security_info.go, security-legacy.yaml, dependency mvdan.cc/sh/v3.

Testing

1. ✅ Per-tool happy paths on a temp workspace and a temp git repo with two commits.

2. 🛡️ Rejections: path escape, symlink escape, refs starting with -, --cont=/etc/passwd as a path never leaks file content, hooks neutralised on git_commit.

3. ⚙️ Config: removed keys error, writes_enabled and scripts loading, script name validation, tool registration per mode.

For More Human Review

  • tools_git.go git_blame passes the path without a literal -- because git rejects --end-of-options <ref> -- <path> on blame; the leading-dash guard in relPath covers it.
  • tools_git.go git_switch emits -c <branch> --end-of-options because -c needs the name right after it.
  • config.go loadSecurityFromFile decodes the yaml twice, once into a map for the removed-key check.

@sonirico
sonirico merged commit 7058d49 into master Sep 4, 2026
1 check passed
@sonirico
sonirico deleted the plan/mcp-shell-0.9.0-typed-tools branch September 4, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant