npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Readme

local-terminal-mcp

npm version CI license: MIT

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 --version

If 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.sh

install.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-mcp

Or 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.json at 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.servers block in user settings.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_rules from local-terminal"

You should see the active allow/deny config. Then:

"run php --version via 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:

  1. config.json next to server.js (or wherever LOCAL_TERMINAL_MCP_CONFIG points). Only used with Option B.
  2. Environment variables in the host's env block.

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

  • allow is replace semantics — your config (or env) wins. A command must match at least one regex.
  • deny is union semantics: final = baked-in dangerous patterns ∪ config.json deny ∪ env DENY_COMMANDS. You can add patterns from any layer; you can never remove the built-ins (sudo, rm -rf /, curl | sh, etc.). Setting DENY_COMMANDS via env will not silently drop denies written in config.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\s is written "^php\\s". Forget this and the server returns bad allow regex from check_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 allow list 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 like echo 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_run call (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.json didn't load, or allow is empty / no ALLOW_COMMANDS env. Call list_rules to 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 your allow patterns and the commands you ask the agent to run, or set PATH explicitly in the host's env block.
  • Hangs on long commands — the timeout is hitting. Raise timeout_default_sec, raise timeout_max_sec, or pass timeout per call.
  • Node version mismatchnode --version must 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 test

Covers 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.

License

MIT