A local MCP server that lets AI agents inspect and drive your running macOS Terminal.app session — list tabs with busy state, read scrollback, execute commands, manage tabs, all gated by a hardened three-tier safety policy plus per-call user confirmation for any write operation. Shell state, env, scrollback persist across calls because it operates on your live Terminal.app, not subprocesses.
See Related work at the bottom for how this compares to other shell-MCP servers.
Fifteen MCP tools across four categories:
| Tool | Read/Write | Description |
|---|---|---|
terminal_list |
read | Enumerate every open Terminal.app tab with tty, title, busy state, and foreground processes. |
terminal_read |
read | Return the full buffer + scrollback of a specific tab, identified by tty. |
terminal_execute |
write | Type a command into a specific tab and press Enter. Refuses if the target tab is busy unless force=true. dry_run=true returns the safety verdict without any side effects. |
terminal_clear |
write | Wipe scrollback of a specific tab via Cmd+K. Briefly steals focus. |
terminal_new_tab |
write | Open a new empty tab in Terminal.app and return its tty for follow-up calls. No dialog — low blast radius (user can close the tab). |
terminal_close_tab |
write | Close a specific tab by tty. Refuses busy tabs unless force=true. No dialog. |
terminal_wait_for_idle |
read | Block until the target tab is no longer busy, or until timeout_seconds (default 60, max 600). Polls every 250ms inside a single JXA call. |
| Tool | Read/Write | Description |
|---|---|---|
safety_list |
read | Show all current safety patterns with their levels. |
safety_add |
write | Propose a new pattern + level; user approves via dialog before persisting. |
safety_remove |
write | Drop a pattern. Warns prominently in the dialog if the pattern is forbidden. |
safety_set_level |
write | Change the level of an existing pattern. Warns when downgrading from forbidden. |
| Tool | Read/Write | Description |
|---|---|---|
pending_list |
read | Snapshot of commands currently awaiting approval. |
pending_approve |
write | Approve a queued command by id. Triggers its own confirmation dialog. |
pending_deny |
write | Deny a queued command. |
| Tool | Read/Write | Description |
|---|---|---|
audit_log_tail |
read | Read the last N entries from the audit log. Default count 20, max 1000. Returns parsed JSON array of {timestamp, tool, outcome, ...} entries. |
Write tools are off by default. Set WRITE_TOOLS_ENABLED=1 in the server's environment to enable them. Even when enabled, every non-safe operation triggers a native macOS confirmation dialog.
- macOS (uses JXA /
osascript) - Node ≥ 20
- Terminal.app (the stock macOS terminal)
Once published:
# Run directly without installing
npx -y @priyanshumit/macos-terminal-mcp
# Or install globally
npm install -g @priyanshumit/macos-terminal-mcpgit clone https://github.com/priyanshumit/macos-terminal-mcp.git
cd macos-terminal-mcp
npm install
npm run buildFirst time the MCP server controls Terminal.app, macOS will prompt for permission:
"node" wants access to control "Terminal".
Click OK. The setting is remembered in System Settings → Privacy & Security → Automation.
terminal_clear, terminal_new_tab, and terminal_close_tab additionally require Accessibility permission — they use System Events to simulate Cmd+K, Cmd+T, and Cmd+W respectively. Grant under System Settings → Privacy & Security → Accessibility.
For terminal_read to return meaningful history, set Terminal.app's scrollback to a generous size:
Terminal.app → Settings → Profiles → (your profile) → Window → Scrollback: set to Unlimited or a large fixed number.
claude mcp add macos-terminal --command=npx --args=-y --args=@priyanshumit/macos-terminal-mcpOr add to ~/.claude.json / project .mcp.json manually:
{
"mcpServers": {
"macos-terminal": {
"command": "npx",
"args": ["-y", "@priyanshumit/macos-terminal-mcp"]
}
}
}To enable write tools, add the env block:
{
"mcpServers": {
"macos-terminal": {
"command": "npx",
"args": ["-y", "@priyanshumit/macos-terminal-mcp"],
"env": { "WRITE_TOOLS_ENABLED": "1" }
}
}
}Restart Claude Code. You should see the fifteen tools listed.
Every terminal_execute call is evaluated against a list of regex patterns, each tagged with a level:
| Level | Behavior |
|---|---|
safe |
Auto-run, no confirmation |
requires_approval |
Native confirmation dialog (also enqueued for async approval via pending_*) |
forbidden |
Refused outright — no dialog can approve it. To run a forbidden command, do it yourself in a real terminal. |
Evaluation rule: highest-restriction wins. If a command matches both a safe pattern and a forbidden pattern, it's forbidden. This blocks composite-command bypasses like ls && rm -rf /tmp/x — the safe ^ls match doesn't shield the forbidden \brm\s+-rf?\b match.
Default if no pattern matches: requires_approval. New/unknown commands always confirm.
| Pattern | Why |
|---|---|
\brm\s+-rf?\b |
Recursive delete |
\bsudo\b |
Privilege escalation — humans only |
|\s*(bash|sh|zsh)\b |
Pipe-to-shell — common attack vector |
\bcurl\b[^|;]*|, \bwget\b[^|;]*| |
Curl/wget piped to anything |
>\s*/etc/, >\s*/dev/ |
Writing to /etc or /dev |
/etc/passwd, /etc/shadow, ~/.ssh |
System or credential files |
\bdd\s+if= |
dd — can overwrite disks |
\bgit\s+push\s+(--force|-f)\b |
Force push |
\bgit\s+reset\s+--hard\b |
Discards local work |
\bgit\s+clean\s+-[fdx]+\b |
Destructive git clean |
\bshutdown\b, \breboot\b, \bkillall\b |
System control |
:\(\)\{:|:&\};: |
Fork bomb |
The full list is in src/safety/patterns.ts. Customize via safety_* tools or by editing ~/.config/macos-terminal-mcp/safety.json directly.
From within Claude:
"Add
^cargo buildas a safe pattern." → Claude callssafety_add({pattern: "^cargo build", level: "safe"})→ you click Allow on the dialog → it's persisted.
"Show me the current safety policy." →
safety_listreturns the JSON.
"Forbidden
^docker\\s+rm." → Claude callssafety_add({pattern: "^docker\\s+rm", level: "forbidden"})→ dialog → persisted.
By editing the file:
~/.config/macos-terminal-mcp/safety.json:
{
"patterns": [
{ "pattern": "^cargo build\\b", "level": "safe", "description": "Rust builds" },
{ "pattern": "\\bmy-deploy-script\\b", "level": "forbidden", "description": "Never deploy from AI" }
]
}If the file has the v1 schema ({"allowlist": [...], "denylist": [...]}), it's automatically migrated on load.
When terminal_execute triggers requires_approval, two things happen in parallel:
- A native macOS dialog pops asking you to Allow/Deny.
- The command is enqueued — visible via
pending_list, resolvable viapending_approve(id)/pending_deny(id, reason).
Whichever path resolves first wins. For solo desktop use, the dialog is the canonical signal. For headless/remote/team contexts, the queue tools provide an out-of-band approval path.
Pending entries auto-expire after 10 minutes. The queue is in-memory only — a server restart drops all pending entries.
No network, no telemetry. The server speaks MCP over stdio to your local Claude client. It makes zero outbound network calls — no analytics, no crash reporting, no auto-update pings.
What is stored on disk:
| File | Path | Contents | Permissions |
|---|---|---|---|
| Audit log | ~/.local/state/macos-terminal-mcp/audit.log |
One JSON line per write-tool call: tool name, tty, full command text, safety level, matched pattern, outcome, timestamp | 0o600 (owner read/write only); parent dir 0o700 |
| Safety config | ~/.config/macos-terminal-mcp/safety.json |
User-added regex patterns with their levels and descriptions | default umask |
Important caveat for secrets handling: terminal_execute writes the full command text to the audit log. If a command contains a password, API key, or other secret, that secret ends up on disk in plaintext (owner-readable only). Mitigations:
- Delete the log at any time:
rm ~/.local/state/macos-terminal-mcp/audit.log - Default forbidden patterns already block common secret-leak vectors (
>\s*/etc/,\bcurl\b[^|;]*\|,\|\s*(bash|sh|zsh)\b,~/.ssh,/etc/passwd,/etc/shadow) - Add your own forbidden patterns for company-specific secret formats via
safety_add
Where the audit log helps: post-hoc review of what an AI agent ran on your machine, debugging unexpected command refusals, and satisfying audit requirements in regulated environments.
Once registered, from Claude Code:
- "List my open terminals." →
terminal_list - "What's the last 50 lines from /dev/ttys062?" →
terminal_read({tty: "/dev/ttys062", lines: 50}) - "Run
git statusin /dev/ttys054." →terminal_execute(auto-runs, safe pattern) - "Run
rm -rf /tmp/cachein /dev/ttys054." → refused (matches forbidden pattern) - "Run
cargo testin /dev/ttys054." → dialog pops (no safe pattern matches, default requires_approval) - "Show me what's in the approval queue." →
pending_list - "Approve queued command abc-123." →
pending_approve({id: "abc-123"})— also pops a dialog
"osascript exited 1: ... not authorized" Automation permission has not been granted. Open System Settings → Privacy & Security → Automation, find the process, allow it to control Terminal.
Confirmation dialogs never appear The MCP server is in a context without a GUI session. Test:
osascript -l JavaScript -e 'Application.currentApplication().includeStandardAdditions = true; Application.currentApplication().displayDialog("test")'terminal_clear does nothing
Requires Accessibility permission, and the target window must accept keyboard focus. If you have multiple Terminal windows, focus may not settle on the intended one before the keystroke fires. Run terminal_list first to confirm the tty.
terminal_read returns less than expected
Terminal.app's scrollback cap is profile-controlled. Increase under Terminal.app → Settings → Profiles → Window → Scrollback.
Command refused as "forbidden" but I have a legit reason
Forbidden patterns intentionally cannot be approved via the tool. Either run the command yourself in a real terminal, or use safety_set_level({pattern: "...", level: "requires_approval"}) to downgrade — you'll see a downgrade warning in the confirmation dialog.
Several shell-MCP servers exist in adjacent niches. The right pick depends on which terminal you use and whether your workflow needs shell state to persist between calls.
| Project | Niche | When to pick it |
|---|---|---|
cfdude/mac-shell-mcp |
Subprocess shell with the same three-tier safety pattern (safe / requires_approval / forbidden) + approval queue. | One-shot commands without state. Simpler architecture; same safety tiers. |
ferrislucas/iterm-mcp |
iTerm2 driver with write_to_terminal, read_terminal_output, send_control_character. ~556 stars. |
You use iTerm2 and are comfortable supervising the agent yourself (no built-in safety policy by design). |
steipete/macos-automator-mcp |
Generic AppleScript/JXA runner, 200+ prebuilt recipes, includes a terminal_app_run_command_new_tab helper. |
You want broad macOS automation beyond just Terminal.app, and accept that there's no command-level safety policy. |
joshrutkowski/applescript-mcp / peakmojo/applescript-mcp / adamrdrew/applescript-mcp |
Generic AppleScript MCPs spanning Calendar, Clipboard, Finder, Notifications, etc. | You want macOS app automation, not specifically Terminal.app. |
macos-terminal-mcp (this) |
Drives your running Terminal.app session — tab list with busy state, scrollback read, tab management, hardened three-tier safety policy. | You want the agent to operate inside your live shell with state preserved across calls, and you want defense-in-depth (ReDoS guard, NFKC normalization, dialog-injection sanitization, audit log). |
The three-tier safety pattern itself originated in cfdude/mac-shell-mcp (or thereabouts) — this project takes that approach and layers on additional defenses plus the live-session Terminal.app integration. The two tools are complementary; pick by workflow.
MIT — see LICENSE.
