bash-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@bash-mcprun ls -lah /tmp and show me the output"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
bash-mcp
A small MCP server that exposes WSL bash as a structured tool to MiniMax Code. Replaces the old wsl -d ... -- bash -c "..." PowerShell pattern with a single tool call.
Why
Running WSL commands from PowerShell (wsl -d Ubuntu-22.04 -- bash -lc "...") has daily friction costs:
Quoting hell — PowerShell reinterprets
$()asGet-Date, backticks, and regex escapes break.UTF-16 garbage —
wsl.exeto PowerShell pipe occasionally emits garbled output.Stateless shells — each call is a fresh shell; no cwd, env, or history.
Output is unstructured — stdout/stderr mixed, no exit code, no duration.
No safety net — a single bad
rm -rfquote wipes the home.
bash-mcp solves all of these with one tool call returning {stdout, stderr, exit_code, duration_ms, timed_out, truncated, classification, audit_id}.
Related MCP server: AdminMCP
License
What's new in v0.3.0
Audit log rotation —
audit.jsonlrotates at 25 MB (configurable) keeping 5 backups (audit.jsonl.{1..5}). Lazy + thread-safe.Concurrency limit — max 8 concurrent subprocesses via
threading.BoundedSemaphoreat the executor boundary. Excess calls queue, never drop.bash_mcp_statusextended withaudit.{max_bytes, backup_count, backups_present}andconcurrency.{max_concurrent, active}fields.95 tests passing (up from 84).
What's new in v0.8.0
Per-project cwd allowlists via
.bash-mcp.toml— drop a TOML file at your repo root (or any ancestor) and bash-mcp picks it up for calls whose cwd lives under that directory. Two modes:extend(default; merge with global) andreplace(sandbox; replace global). Zero behavior change when no file is present. See Configuration →.bash-mcp.tomlbelow for the schema. No new MCP tool — v0.8 is purely a config-driven feature.
What's new in v0.7.1
bash_mcp_audit_read(backup_index, max_entries?, tool_filter?)— closes the v0.7 gzip one-way archival caveat. Transparently decompressesaudit.jsonl.N.gzbackups in addition to plaintext. Returns a stable{backup_index, path, format, entries, returned}object. Audited astool="bash_mcp_audit_read".Cleanup — deleted stray
infra/install.sh.before-addendum(untracked, never committed).12 tools total (up from 11). 289 tests passing (up from 274). 86 smoke cases (up from 78).
All changes are non-breaking. v0.7.0 callers see zero behavior change.
What's new in v0.7.0
bash_mcp_classify(command)denylist explainer — read-only tool that returns{class, matched_pattern, pattern_index, hint_with_dangerous_false, hint_with_dangerous_true, would_execute, would_execute_with_dangerous_true, audit_id}. Use BEFOREbash_run_commandif you're unsure whether a command will be rejected. Saves a round-trip and gives the exact regex that would fire.Session auto-TTL (opt-in) — set
BASH_MCP_SESSION_IDLE_TIMEOUT_Sto enable. A daemon janitor evicts sessions whoselast_used_atis older than the timeout. Each eviction is audited astool="bash_mcp_session_destroy" reason="idle_ttl_expired". Default 0 = disabled (preserves v0.6 contract).Audit log gzip-on-rotation (opt-in) — set
BASH_MCP_AUDIT_GZIP_THRESHOLD_BYTESto enable. After rotation, any backup larger than the threshold is gzipped to.jsonl.N.gz(one-way; usezcatto inspect). Default 0 = disabled (preserves v0.3 plaintext-only behavior).bash_mcp_statusextended withaudit.{gzip_threshold_bytes, gzip_enabled}andsessions.{idle_timeout_s, janitor_enabled}.274 tests passing (up from 220).
All three features are opt-in via env vars and default to off, so existing callers see zero behavior change.
What's new in v0.6.0
Stateful sessions — four new tools (
bash-mcp_session_create,_run,_destroy,_list) keep a server-sidecwdand accumulatedenvdict across calls. Use them when you need tocdsomewhere and stay there, orexporta variable and have it persist for the next call.Sessions are process-lifetime — a server restart wipes them. Caller is responsible for
_destroy. There is no auto-TTL.Best-effort parser — top-level
cd PATH,export VAR=value,unset VARare tracked; shell scripts, functions,$(...), heredocs are out of scope (the subprocess still runs correctly, but state isn't persisted from them).bash_mcp_statusextended withsessions.{active, max_env_per_session}.220 tests passing (up from 164).
See CHANGELOG.md for full history.
What's new in v0.4.0
Quarterly denylist review #1 — 6 new HARD patterns + 4 new SOFT patterns (see Safety table below).
Denylist review history in
safety.pydocumenting all reviews to date (v0.1, v0.2, v0.3, v0.4).164 tests passing (up from 95).
Tools
Tool | Purpose |
| Execute bash. Returns structured output. |
| OS, kernel, Python, uv, PATH entries. |
| ~37 well-known tools with |
| Resolve a single binary. |
| Service health, audit stats + rotation config, concurrency, sessions, uptime, tool list. |
| Create a stateful session; returns a |
| Run a command in a session; persists |
| Destroy a session, free memory. |
| List active sessions, most-recently-used first. |
| Read-only denylist explainer; returns |
| Read entries from a rotated audit backup (plaintext or gzip). v0.7.1. |
All tools are namespaced as bash-mcp_* in MiniMax Code.
Quick start (for the agent)
// Discover environment
bash-mcp_check_env()
// Check if a tool is installed
bash-mcp_list_binaries() // batch
bash-mcp_which({name: "rg"}) // single
// Run a command
bash-mcp_run_command({command: "ls -lah /tmp"})
// Run with custom cwd and timeout
bash-mcp_run_command({command: "pytest -xvs", cwd: "/home/jbecerra/projects/myapp", timeout_ms: 120000})
// Bypass the SOFT denylist
bash-mcp_run_command({command: "sudo systemctl restart nginx", dangerous: true})
bash-mcp_run_command({command: "pip uninstall requests", dangerous: true})
// Verify service health (audit + concurrency state)
bash-mcp_status()⚠️ DO NOT use wsl -d ... -- bash -c "..."
A PreToolUse hook in MiniMax Code denies any shell call whose command starts with wsl(\.exe)?\s. If you trigger it, you'll see an abort reason pointing you to this README.
Escape hatch (for genuine testing): prefix the command with BASH_MCP_SKIP=1.
Architecture
MiniMax Code ──HTTP POST──> bash-mcp server (WSL, port 54321)
│
├─ classify(command) → safe / dangerous / reject
├─ acquire concurrency.slot() (BoundedSemaphore, max 8)
├─ subprocess.run([bash, "-lc", command], ...)
├─ truncate stdout/stderr to 50KB
├─ audit.log(...) → audit.jsonl
└─ on size > 25 MB: rotate → audit.jsonl.{1..5}Streamable-http transport. bash-mcp_run_command is stateless per call; the
four bash-mcp_session_* tools additionally clone + write cwd and env
through src/bash_mcp/sessions.py (process-lifetime only). v0.7 adds a
daemon janitor thread (opt-in via BASH_MCP_SESSION_IDLE_TIMEOUT_S) and
gzip-on-rotation for the audit log (opt-in via
BASH_MCP_AUDIT_GZIP_THRESHOLD_BYTES).
Safety
Full list lives in src/bash_mcp/safety.py. Snapshot:
HARD denylist (always rejected, cannot be bypassed):
Category | Patterns |
Block-device destruction |
|
Process / system | fork bombs ( |
Unrecoverable delete |
|
Supply chain |
|
SOFT denylist (requires dangerous=true):
Category | Patterns |
Privilege / process |
|
System changes |
|
Package removal (v0.4) |
|
Package install |
|
Destructive sync (v0.4) |
|
Every error response includes hint (actionable next step) and documentation (path to the skill) — the agent does not need to parse free text to recover.
cwd is allowlist-validated: must resolve under $HOME, /tmp, /home, /mnt/c/Users/jesus, or /var/tmp. Windows-style paths (C:\foo) are auto-converted to /mnt/c/foo.
Denylist is reviewed quarterly. Last review: v0.4 (2026-09-12) — see safety.py for full history.
Configuration
All via environment variables. Set in ~/.config/systemd/user/bash-mcp.service (Environment= lines), or before running locally.
Variable | Default | Purpose |
|
| Bind address |
|
| Bind port (IANA dynamic range) |
|
| Audit log rotation threshold |
|
| Audit log backups kept |
|
| Max in-flight subprocesses |
.bash-mcp.toml (per-project cwd allowlist, v0.8)
Drop a .bash-mcp.toml at any directory in your project. bash-mcp walks up
from the call's cwd to find it (stops at $HOME or filesystem root, max 32
hops) and applies the resulting allowlist to that call. No new tool, no
schema-validation library, no install hook.
Schema:
# mode is optional. Defaults to "extend".
mode = "extend" # or "replace"
# required: list of paths the project's cwds may resolve under.
# Relative entries resolve against the TOML's parent dir;
# absolute entries (after ~ expansion) are used as-is.
# Existence is NOT checked here — executor.run checks separately.
allowed_roots = [".", "frontend", "backend", "/srv/shared-cache"]Modes:
extend(default) — add the project'sallowed_rootsto the globalALLOWED_CWD_ROOTS. Use this when your project needs to expose additional worktrees, build dirs, or shared caches outside the global allowlist (e.g./srv/shared-cacheabove).replace— replace the global list with the project's roots. Use this for sandboxed environments where the global list is too permissive. An emptyallowed_rootslist is a deliberate lockout (no cwd is allowed; the caller receivesINVALID_CWD).
Failure modes (all fail-soft — bash-mcp falls back to the global allowlist, no crash):
Malformed TOML → stderr warning, global roots apply.
Unknown
modevalue → stderr warning, defaults toextend.allowed_rootsis not a list of strings → stderr warning, global roots apply.
Cache: the loaded allowlist is cached per (start_dir, mtime_ns) for
the lifetime of the bash-mcp process. Editing the TOML invalidates the
cache; deleting the TOML means the next call from that directory re-walks
the tree.
Deploying
infra/install.sh deploys everything to a fresh WSL/Windows setup:
systemd unit →
~/.config/systemd/user/bash-mcp.servicePreToolUse hook →
~/.minimax/agents/mavis/hooks/bash-mcp-redirect.mdSkill →
~/.mavis/skills/bash-mcp/mcp.json entry →
~/.minimax/mcp/mcp.jsonAgent system_prompt addendum → reported on stdout by
install.sh [5/5]; applied manually once per environment via the desktopmavis agent update mavistool (the WSL and WindowsmavisCLIs do not expose theagentsubcommand)
The script also prints the one mavis mcp create command that must be
run from Windows PowerShell (the runtime registry is Windows-side).
./infra/install.shAgent integration
bash-mcp ships an infra/system-prompt-addendum.md that, when applied, is
appended to the mavis agent's systemPrompt. This is the agent-side guardrail
that prevents the model from falling back to the wsl -d ... -- bash -c "..."
pattern from PowerShell — even when the PreToolUse hook times out or the skill
isn't auto-loaded.
Auto-apply is not supported. The mavis CLI on WSL is the IDE launcher
(start / stop / status only), and the Windows mavis.cmd references
daemon/cli.js which is not bundled in current installs. Neither CLI exposes
agent update. The apply must be performed by the desktop mavis MCP tool,
which is what install.sh [5/5] documents on stdout.
Manual apply (run once per environment, anywhere with the desktop tool):
# install.sh prints the exact 3-step command (snapshot → build → apply)
# on stdout. The canonical addendum is at infra/system-prompt-addendum.md.
# Idempotent: rerun is a no-op if the marker substring is already in system_prompt.Why this lives in bash-mcp, not in the agent repo. bash-mcp is the project
that owns the bash-mcp-vs-wsl-d rule across all three sync points (skill,
hook, mcp.json description). The system_prompt addendum is the fourth sync
point — same wording, same escape hatch (BASH_MCP_SKIP=1), same cross-references
to this README. Keeping all four together is what AGENTS.md means by "four places
that must stay in sync".
Operations
Start / stop / restart
systemctl --user start bash-mcp.service
systemctl --user stop bash-mcp.service
systemctl --user restart bash-mcp.service
systemctl --user status bash-mcp.service
# Logs (via screen session)
screen -r bash-mcp
# Or via journalctl
journalctl --user -u bash-mcp.service -fAudit log (rotation in v0.3+)
The audit log at ~/.local/share/bash-mcp/audit.jsonl rotates automatically when it crosses BASH_MCP_AUDIT_MAX_BYTES (default 25 MB).
# Tail the current log
tail -f ~/.local/share/bash-mcp/audit.jsonl
# Pretty-print last 5 entries
tail -5 ~/.local/share/bash-mcp/audit.jsonl | jq .
# Check current size + rotation config
bash-mcp_status # → audit.size_bytes, audit.max_bytes, audit.backup_count, audit.backups_present
# List existing backups (audit.jsonl.{1..5})
ls ~/.local/share/bash-mcp/
# Read a specific backup
less ~/.local/share/bash-mcp/audit.jsonl.1 | jq .Rotation behavior:
Lazy — happens on next
log()call after the threshold is crossed.Thread-safe —
audit.jsonland the rotation move happen under one lock.Fail-soft —
OSErroron rename prints to stderr; next call retries.Naming —
audit.jsonl.{N}(stdlib convention); oldest is dropped when count exceedsBASH_MCP_AUDIT_BACKUP_COUNT.No content loss — verified by
test_rotation_preserves_content.
Concurrency (v0.3+)
Max 8 concurrent subprocesses by default. Excess calls queue (don't drop). Tune via BASH_MCP_MAX_CONCURRENT.
# Check current limit + active count
bash-mcp_status # → concurrency.max_concurrent, concurrency.activeIf you make 20 parallel tool calls, 8 run, 12 queue. The semaphore is at the executor boundary, so bash_run_command, bash_check_env's version probes, and any future subprocess-using tool all share the same cap.
Health check (no SSH/curl needed)
# Via MiniMax Code (recommended):
bash-mcp_status
# Or via shell + curl:
curl http://localhost:54321/mcp/ # then initialize + tools/call bash_mcp_statusWSL IP changed (after reboot)
cd /home/jbecerra/projects/bash-mcp
./infra/update-ip.shThis rewrites only the bash-mcp URL in ~/.minimax/mcp/mcp.json (and ~/.cursor/mcp.json if present). Does not touch semantic-memory.
Bootstrap on Windows login
infra/launcher.sh ships out of the box. To run it automatically on every Windows logon, register the scheduled task (run as Administrator from PowerShell):
powershell -ExecutionPolicy Bypass -File C:\path\to\bash-mcp\infra\windows\install-task.ps1This registers BashMcp-WSL-Bootstrap in Task Scheduler with a 60s delay, RunLevel=Highest, action = wsl.exe /home/jbecerra/projects/bash-mcp/infra/launcher.sh. The launcher ensures bash-mcp.service is running (idempotent: systemd skips if already running).
Verify:
Get-ScheduledTask -TaskName BashMcp-WSL-Bootstrap | Format-ListRemove:
Unregister-ScheduledTask -TaskName BashMcp-WSL-Bootstrap -Confirm:$falseThe install.sh script (when run without --dry-run) prints the same install-task.ps1 path as a hint.
Development
Install
source ~/.local/bin/env
export PATH="$HOME/.local/bin:$PATH"
cd /home/jbecerra/projects/bash-mcp
uv syncRun locally
uv run python -m bash_mcp.server --transport streamable-http --host 0.0.0.0 --port 54321Run tests
uv run pytest tests/ -v164 tests cover:
115 safety cases (test_safety.py) — parametrized denylist + safe baselines + review-history
27 executor cases (test_executor.py) — subprocess + cwd allowlist + Windows→WSL
9 error envelope cases (test_errors.py)
6 audit log rotation cases (test_audit_rotation.py)
5 concurrency limit cases (test_concurrency.py)
11 e2e tests against the live server (test_e2e.py)
Project layout
bash-mcp/
├── AGENTS.md # project-scoped rules for future agents
├── CHANGELOG.md # version history
├── LICENSE # MIT
├── README.md # this file
├── pyproject.toml # uv-managed, FastMCP 2.7.0
├── uv.lock
├── src/bash_mcp/ # server code
│ ├── server.py # FastMCP + 6 tools
│ ├── executor.py # subprocess wrapper, timeout, truncation, cwd allowlist, concurrency slot
│ ├── safety.py # classify(command) + quarterly denylist reviews
│ ├── audit.py # JSONL append-only logger + size-based rotation
│ ├── discovery.py # which / list_binaries (thread-pool)
│ └── concurrency.py # BoundedSemaphore wrapper
├── tests/ # 164 tests
│ ├── test_safety.py # 115 parametrized denylist cases
│ ├── test_executor.py # 27 subprocess + cwd allowlist
│ ├── test_errors.py # 9 error envelope shape + hints
│ ├── test_audit_rotation.py # 6 rotation cases
│ ├── test_concurrency.py # 5 concurrency limit cases
│ └── test_e2e.py # 11 live-server round-trips
└── infra/ # deployment artifacts
├── install.sh # idempotent deploy (--dry-run supported)
├── launcher.sh # WSL bootstrap (parallel to semantic-memory-launcher.sh)
├── update-ip.sh # refresh WSL IP in mcp.json
├── mcp.json.snippet # documentation reference
├── systemd/
│ └── bash-mcp.service
├── hooks/
│ ├── bash-mcp-redirect.md
│ └── bash-mcp-redirect.js
├── skills/
│ └── bash-mcp/
│ ├── SKILL.md
│ └── _meta.json
└── windows/ # Windows-side deployment helpers
├── BashMcp-WSL-Bootstrap.xml # Scheduled Task template
└── install-task.ps1 # PowerShell installer for the taskAdd a new tool
from bash_mcp import audit
@mcp.tool
def bash_my_new_tool(...) -> dict:
"""Docstring becomes the tool description for the agent."""
...If your tool calls subprocess.run, wrap it in concurrency.slot() so it participates in the global cap:
from bash_mcp.concurrency import slot as concurrency_slot
with concurrency_slot():
subprocess.run([...])Add a denylist pattern
Add to
REJECT_PATTERNS(hard) orDANGEROUS_PATTERNS(soft) insrc/bash_mcp/safety.py.Add a parametrized test case in
tests/test_safety.py.Update
README.mdsafety table and the deployed~/.mavis/skills/bash-mcp/SKILL.md.Add a dated entry to the review-history block at the top of
safety.pylisting what was added.systemctl --user restart bash-mcp.serviceso the new pattern is live.
For non-trivial gaps or batch additions, treat it as a quarterly review (see AGENTS.md).
Adjust rotation / concurrency limits
Change the systemd unit (
infra/systemd/bash-mcp.service),Environment=BASH_MCP_AUDIT_MAX_BYTES=..., etc.systemctl --user daemon-reload && systemctl --user restart bash-mcp.serviceVerify via
bash_mcp_status()that the new values are loaded.
Test the hook manually
# Should deny (abort)
echo '{"input":{"toolName":"bash","toolArgs":{"command":"wsl -d Ubuntu -- bash -c \"echo hi\""}},"output":{}}' \
| wsl -d Ubuntu-22.04 -- node /home/jbecerra/projects/bash-mcp/infra/hooks/bash-mcp-redirect.js
# => {"_abort":{"reason":"..."}}
# Should pass
echo '{"input":{"toolName":"bash","toolArgs":{"command":"git status"}},"output":{}}' \
| wsl -d Ubuntu-22.04 -- node /home/jbecerra/projects/bash-mcp/infra/hooks/bash-mcp-redirect.js
# => {}
# Should pass (escape hatch)
echo '{"input":{"toolName":"bash","toolArgs":{"command":"BASH_MCP_SKIP=1 wsl echo"}},"output":{}}' \
| wsl -d Ubuntu-22.04 -- node /home/jbecerra/projects/bash-mcp/infra/hooks/bash-mcp-redirect.js
# => {}Deploy to a fresh WSL machine
git clone <this-repo> /home/jbecerra/projects/bash-mcp
cd /home/jbecerra/projects/bash-mcp
./infra/install.sh
# Then from Windows PowerShell, run the printed `mavis mcp create` command.Out of scope (deferred to v0.9+)
Already shipped:
Stateful sessions with persistent cwd across calls— shipped in v0.6 (bash_mcp_session_create/_run/_destroy/_list). Process-lifetime only; see "What's new in v0.6.0".Windows Scheduled Task XML for login auto-start— shipped in v0.5 atinfra/windows/BashMcp-WSL-Bootstrap.xml+install-task.ps1.Auto-TTL / idle cleanup— shipped in v0.7 (opt-in viaBASH_MCP_SESSION_IDLE_TIMEOUT_S, daemon janitor thread + audit on each eviction). See "What's new in v0.7.0".A "denylist explainer" tool— shipped in v0.7 asbash_mcp_classify(command). Returns{class, matched_pattern, hint, would_execute, ...}without executing. Lets the agent self-check before sending.
Still pending:
Persistent shell state (PTY-based) — v0.6 sessions are best-effort regex parsing of
cd/export/unset. v0.7 could keep a realbashprocess alive per session (viapexpector similar) and track shell variables, aliases, functions,set -e/pipefail, and job control. Requires a redesign of the session lifecycle and a hard cap on concurrent shells (memory + fd cost).Disk persistence for sessions — v0.6 sessions are wiped on server restart. v0.7 could serialize
_SESSIONSto SQLite or JSON-on-disk and restore on boot, with a migration story for callers that depend on the process-lifetime contract.Session snapshots (
forka session at a point in time) — useful for branching workflows.Cross-session env sharing — share a named env dict across multiple sessions.
Web UI for browsing the audit log — minimal Flask/FastAPI page on a separate port with filters by tool / classification / time.
Audit log compression— shipped in v0.7.1 viabash_mcp_audit_read(closes the gzip one-way archival caveat). See "What's new in v0.7.1". Remaining: gzip-on-rotation above a higher threshold + audit log shipping (Loki/CloudWatch) still deferred to v0.9+.Audit log shipping — Loki push API, CloudWatch Logs, or local syslog. Optional, gated by config.
Per-project allowlists(.bash-mcp.tomlin repo root)— shipped in v0.8 via the.bash-mcp.tomlschema. See Configuration →.bash-mcp.tomlabove. Supports bothextend(merge with global) andreplace(sandbox) modes.A formal threat-model document — STRIDE-style analysis of the audit log, the hook, the denylist, and the session lifecycle.
This server cannot be deployed
Maintenance
Related MCP Connectors
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Source-checked CLI guides and model-aware planning for Claude Code, Codex, and Grok Build.
Runtime permission, approval, and audit layer for AI agent tool execution.
Supervised API-write gateway for AI agents with policy, human approval and execution receipts.
Related MCP Servers
- AlicenseAqualityDmaintenanceA Model Context Protocol server that enables Claude Desktop to execute bash commands and scripts in WSL2 Linux distributions on Windows.6MIT
- FlicenseNot gradedqualityNot gradedmaintenanceProvides LLMs with administrative capabilities to execute shell commands on local systems through a pseudo-terminal environment with Unix Domain Socket communication.-
- FlicenseNot gradedqualityDmaintenanceEnables safe execution of terminal commands across different shells (bash, cmd, PowerShell) with configurable timeouts, working directories, and resource limits for command-line operations through AI assistants.-
- AlicenseAqualityBmaintenanceEnables secure command-line interactions on Windows systems with support for PowerShell, CMD, Git Bash, and WSL shells, providing controlled file access, command execution, and configurable security restrictions.637 npm4MIT