local-terminal-mcp
v1.2.0
Published
MCP server that runs allowlisted shell commands on your Mac from Claude (Cowork VM bridge). Stdio, regex allow/deny, audit log.
Downloads
254
Maintainers
Readme
local-terminal-mcp
An MCP server (Node.js) that lets an AI agent run allowlisted shell commands on your local machine — even when the agent itself runs in a sandbox, container, or VM that can't reach your host shell.
Use it for things like php artisan octane:reload, composer install, ./rr reset, git status — commands that need your real toolchain, your gh auth, your project deps, your environment.
Why this exists
Several AI coding tools (Claude's Cowork VM, agent sandboxes, devcontainer-based workflows) intentionally isolate the agent's shell from your host. That's good for safety, but it also means the agent can't run the real php, composer, git, or whatever else lives on your machine.
This MCP server runs natively on your host, exposes four tools — shell_run, check_command, reload_config, list_rules — over stdio, and is launched on demand by your MCP host (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, …). The agent calls a tool; your host executes the command; output flows back. No HTTP, no open ports, no inbound network surface.
Requirements
Node 18+ and npm:
node --version
npm --versionIf you don't have Node, install from https://nodejs.org or via Homebrew (brew install node).
Install
Two paths — pick one.
Option A — npx (zero install, recommended)
Nothing to clone. Your MCP host runs npx -y local-terminal-mcp on demand and you configure the allow/deny rules via env vars in the host config. Jump to Install in your MCP host.
Option B — Local clone
For when you'd rather edit a config.json file than wrangle regex escapes inside JSON:
git clone https://github.com/amims71/local-terminal-mcp.git ~/code/local-terminal-mcp
cd ~/code/local-terminal-mcp
./install.shinstall.sh runs npm install and bootstraps config.json from config.example.json if you don't have one yet. Edit config.json to set your default_cwd and tighten the allow list to the commands you actually use.
Install in your MCP host
Most MCP hosts use the same mcpServers block shape. Pick the section that matches your host, swap ~/projects/your-app for the absolute path you want commands to run in by default, and adjust the regex list to match the commands you actually need.
Claude Desktop
Config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"local-terminal": {
"command": "npx",
"args": ["-y", "local-terminal-mcp"],
"env": {
"ALLOW_COMMANDS": "^php\\s+artisan\\s,^composer\\s+(install|update|require),^git\\s+(status|diff|log)\\b",
"DENY_COMMANDS": "\\bproduction\\b",
"LOCAL_TERMINAL_DEFAULT_CWD": "~/projects/your-app",
"LOCAL_TERMINAL_TIMEOUT_DEFAULT": "60"
}
}
}
}Fully quit Claude Desktop (⌘Q on macOS — not just close the window) and reopen. MCP servers are only loaded on app start.
Claude Code (CLI)
claude mcp add local-terminal \
-e ALLOW_COMMANDS='^git\s+(status|diff|log)\b,^php\s+--version$' \
-e LOCAL_TERMINAL_DEFAULT_CWD=~/projects/your-app \
-- npx -y local-terminal-mcpOr per-project: drop a .mcp.json at the repo root with the same mcpServers shape as Claude Desktop and Claude Code will pick it up.
Cursor
- Global:
~/.cursor/mcp.json - Per-project:
.cursor/mcp.jsonat the repo root
Use the same mcpServers block as Claude Desktop. Cursor reloads MCP servers automatically; if it doesn't, toggle the server off/on in Settings → MCP.
Windsurf
Edit ~/.codeium/windsurf/mcp_config.json — same mcpServers schema. Restart Windsurf.
VS Code (native MCP)
- Workspace:
.vscode/mcp.json - User-wide: an
mcp.serversblock in usersettings.json
VS Code uses a slightly different schema — servers (not mcpServers) and an explicit type:
{
"servers": {
"local-terminal": {
"type": "stdio",
"command": "npx",
"args": ["-y", "local-terminal-mcp"],
"env": {
"ALLOW_COMMANDS": "^git\\s+(status|diff|log)\\b",
"LOCAL_TERMINAL_DEFAULT_CWD": "~/projects/your-app"
}
}
}
}Reload the window or click Restart on the server in the MCP panel.
Local-clone variant
If you went with Option B, the command/args change to your local server.js. Allow/deny lives in config.json, so the env block becomes optional:
{
"mcpServers": {
"local-terminal": {
"command": "node",
"args": ["/absolute/path/to/local-terminal-mcp/server.js"]
}
}
}Most hosts will not expand ~ inside JSON config values, so use an absolute path here (run pwd inside your clone to get it).
PATH gotcha (macOS)
If node/npx isn't on the PATH your MCP host inherits (common with Homebrew/nvm Node and GUI apps launched from Finder/Dock), replace "command": "npx" with the absolute path:
"command": "/opt/homebrew/bin/npx"Find yours with which npx. Same applies to node if you use Option B.
Test it
From a fresh agent chat:
"call
list_rulesfrom local-terminal"
You should see the active allow/deny config. Then:
"run
php --versionvia local-terminal"
You should see your real PHP version. If you see rejected by policy, the command didn't match any allow regex — adjust your rules and restart the host.
Configuration
The server reads config from two places. Env vars win over file values:
config.jsonnext toserver.js(or whereverLOCAL_TERMINAL_MCP_CONFIGpoints). Only used with Option B.- Environment variables in the host's
envblock.
config.json shape
{
"default_cwd": "~/projects/your-app", // used when caller doesn't pass cwd
"timeout_default_sec": 60, // per-call default
"timeout_max_sec": 600, // hard ceiling
"max_output_chars": 200000, // truncate large stdout/stderr
"shell": "/bin/zsh",
"allow": [ "^php\\s+artisan\\s", "..." ], // regex strings
"deny": [ "\\bproduction\\b", "..." ] // regex strings
}Env vars (override file values)
| Env var | Value | Notes |
|---|---|---|
| ALLOW_COMMANDS | "<regex1>,<regex2>,..." | Comma-separated regex list. Replaces any file allow. |
| DENY_COMMANDS | "<regex1>,<regex2>,..." | Comma-separated. Added to baked-in deny and file deny (never replaces either). |
| LOCAL_TERMINAL_DEFAULT_CWD | path (supports ~) | |
| LOCAL_TERMINAL_SHELL | absolute path | e.g. /bin/bash |
| LOCAL_TERMINAL_TIMEOUT_DEFAULT | seconds (int) | |
| LOCAL_TERMINAL_TIMEOUT_MAX | seconds (int) | hard cap |
| LOCAL_TERMINAL_MAX_OUTPUT | chars (int) | output truncation |
| LOCAL_TERMINAL_MCP_CONFIG | path | custom config.json location |
Rules
allowis replace semantics — your config (or env) wins. A command must match at least one regex.denyis union semantics: final = baked-in dangerous patterns ∪config.jsondeny ∪ envDENY_COMMANDS. You can add patterns from any layer; you can never remove the built-ins (sudo,rm -rf /,curl | sh, etc.). SettingDENY_COMMANDSvia env will not silently drop denies written inconfig.json.- Patterns are JavaScript regexes (
new RegExp(pat)), searched anywhere in the command string. Anchor with^if you want a strict prefix. - In a JSON env value, every regex
\becomes\\. So^php\sis written"^php\\s". Forget this and the server returnsbad allow regexfromcheck_command.
After edits: restart your MCP host. Then call list_rules to confirm the effective config — it shows which env vars are active.
Tools
Four tools are exposed over stdio. Each response is a JSON-encoded text block.
shell_run
Run a shell command if it passes allow/deny.
Input:
| field | type | default | notes |
|---|---|---|---|
| command | string (required) | — | Full shell command string. Pipes and redirects are interpreted. |
| cwd | string (optional) | default_cwd | Working directory. Must exist. Supports ~ expansion. |
| timeout | int (optional) | timeout_default_sec | Seconds. Capped at timeout_max_sec. |
Output (success):
{
"ok": true,
"exit_code": 0,
"stdout": "...",
"stderr": "",
"cwd": "/absolute/path",
"command": "git status",
"truncated": false,
"duration_sec": 0.124
}Output (non-zero exit) is the same shape with ok: false and the real exit_code.
Output (timeout):
{
"ok": false,
"exit_code": null,
"error": "timed out after 60s",
"stdout": "...",
"stderr": "...",
"cwd": "...",
"command": "...",
"truncated": false,
"duration_sec": 60.012
}Output (rejected by policy):
{
"ok": false,
"error": "rejected by policy: command did not match any allow pattern",
"command": "rm -rf /"
}When output exceeds max_output_chars, the tail is kept and prefixed in-band with ...[truncated, kept last N chars], and truncated: true is set on the response.
check_command
Dry-run an allow/deny decision without executing. Useful before invoking shell_run.
Input: { "command": string }
Output:
{
"command": "git status",
"allowed": true,
"reason": null
}When rejected, reason is a human-readable string like "command did not match any allow pattern" or "command matched deny pattern: \\bsudo\\b".
reload_config
Re-read config.json from disk and re-apply env overrides without restarting the MCP host. Lets you edit config.json, then ask the agent to call reload_config instead of fully quitting and reopening Claude Desktop / Cursor / etc.
The agent cannot write the file — only request a re-read. The file mutation happens externally (you edit it directly, or in this Claude Code session). This preserves the security model: the agent can't expand its own allow list.
Input: none.
Output (success — same shape as list_rules, with ok and reloaded flags added):
{
"ok": true,
"reloaded": true,
"config_path": "/absolute/path/to/config.json",
"config_file_exists": true,
"env_overrides": {},
"default_cwd": "...",
"timeout_default_sec": 60,
"timeout_max_sec": 600,
"max_output_chars": 200000,
"shell": "/bin/zsh",
"allow": ["..."],
"deny": ["..."]
}Output (bad JSON or other read error — the previously-loaded config remains live):
{
"ok": false,
"reloaded": false,
"error": "Bad JSON in /path/to/config.json: Unexpected token..."
}list_rules
Return the effective configuration: paths, timeouts, allow/deny lists, and which env vars are overriding the file. No execution.
Input: none.
Output:
{
"config_path": "/absolute/path/to/config.json",
"config_file_exists": true,
"env_overrides": { "ALLOW_COMMANDS": "..." },
"default_cwd": "...",
"timeout_default_sec": 60,
"timeout_max_sec": 600,
"max_output_chars": 200000,
"shell": "/bin/zsh",
"allow": ["..."],
"deny": ["..."]
}Security notes
- This is a real RCE endpoint for your machine. It runs as your user, with your env, your shell. Treat the
allowlist as you would a sudoers file — review each pattern. - The server reads only what your MCP host tells it to — there is no auto-discovery, no remote control plane. It's a pure stdio child process.
- Use anchored patterns (
^php\s+artisan\s) so attackers can't sneak past with prefix tricks likeecho x; rm -rf /. - Commands run with
shell: "/bin/zsh"so pipes/redirects work. That means;,|,&&, command substitution etc. are all interpreted. Your allow list must account for this. - Built-in deny rules block the worst footguns:
sudo,rm -rf /, fork bombs,dd,mkfs, reboot/halt,curl | sh. Don't try to remove them. - Every
shell_runcall (allowed and rejected) is logged to stderr with a timestamp, cwd, timeout, and the command string. Claude Desktop captures that to~/Library/Logs/Claude/mcp-server-local-terminal.log; other hosts have their own log locations.
Troubleshooting
- "No allow patterns configured" — your
config.jsondidn't load, orallowis empty / noALLOW_COMMANDSenv. Calllist_rulesto see where the server is reading from. - MCP doesn't appear in your host — fully quit and reopen (just closing the window isn't enough for Electron-based hosts). Check the host's MCP log directory.
- "command not found" — the MCP runs with the env your MCP host was launched with. If a command works in your terminal but not here, it's almost always
PATH. Use absolute paths (/opt/homebrew/bin/php) in both yourallowpatterns and the commands you ask the agent to run, or setPATHexplicitly in the host'senvblock. - Hangs on long commands — the timeout is hitting. Raise
timeout_default_sec, raisetimeout_max_sec, or passtimeoutper call. - Node version mismatch —
node --versionmust be 18 or higher.
Updating
Option A (npx): nothing to do — npx -y local-terminal-mcp always grabs the latest published version on next host launch. Pin a specific version with "args": ["-y", "[email protected]"] if you'd rather control upgrades.
Option B (local clone):
cd ~/code/local-terminal-mcp
git pull
npm install
# Then restart your MCP host.Tests
npm testCovers checkCommand (allow/deny semantics, malformed regex), loadConfig (file/env precedence, deny union, bad JSON), and runShell (exit codes, stderr capture, timeout shape, truncation marker). Uses Node's built-in test runner — no extra dev deps.
