Skip to content

chrisscott/bash-mcp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bash-mcp

An MCP server that runs allowlisted shell commands. Designed to fail closed: unknown commands are rejected, arguments are validated against per-command rules, and execution never goes through a shell.

Security model

Every call is filtered through layers that must all pass:

  1. Command allowlist — only commands listed in commands are runnable. Shells and interpreters (sh, bash, env, sudo, python, node, perl, …) are hard-denied even if listed.
  2. No shell interpretation — commands are executed via spawn(cmd, argv, { shell: false }). ls; rm -rf ~ is a single argument, not a pipeline.
  3. Argument validation — per-command argPattern (regex), subcommands allowlist, maxArgs, and a default denylist of shell metacharacters (;|& ` $<>() newlines, tabs, backslash) and NUL bytes. -c, --exec, --eval, -e are always rejected.
  4. cwd confinement — execution is pinned to the configured cwd. Optional per-call cwd override must resolve under that root.
  5. Minimal env — the parent process env is not inherited. Only keys from the config's env are passed.
  6. Timeouts & output caps — SIGTERM on timeout, SIGKILL 2s later. stdout/stderr truncated to maxOutputBytes.
  7. Audit log — every accepted and rejected call is appended to a JSONL file when auditLogPath is set.

Known footguns

Allowlisting certain commands effectively defeats the allowlist. Avoid these unless you know what you're doing:

  • find (via -exec), xargs
  • make, npm, yarn, pnpm, pip — they run scripts from files under cwd
  • git with unrestricted subcommands — git config core.sshCommand=… then git fetch executes arbitrary code; restrict via subcommands
  • Any editor or pager that can shell out (vim, less with !)

The example config (examples/readonly-git.config.json) restricts git to read-only subcommands.

Install

npm install
npm run build

Configure

Create a JSON config. The schema at schema/config.schema.json provides editor validation and hover docs — reference it via $schema or a .vscode/settings.json mapping.

{
  "$schema": "./schema/config.schema.json",
  "cwd": "/absolute/path/to/workdir",
  "timeoutMs": 30000,
  "maxOutputBytes": 1048576,
  "env": { "PATH": "/usr/bin:/bin", "HOME": "/tmp" },
  "auditLogPath": "/tmp/bash-mcp/audit.log",
  "commands": {
    "git": {
      "subcommands": ["status", "diff", "log", "show", "rev-parse"],
      "argPattern": "^[A-Za-z0-9._/@:=-]+$",
      "maxArgs": 10
    },
    "ls":  { "argPattern": "^[A-Za-z0-9._/-]+$", "maxArgs": 5 },
    "cat": { "argPattern": "^[A-Za-z0-9._/-]+$", "maxArgs": 3 },
    "rg":  { "anyArgs": true, "maxArgs": 10 }
  }
}

Field reference

Field Type Notes
cwd string Absolute path. All execution confined here.
timeoutMs integer Default 30000. Max 600000.
maxOutputBytes integer Per-stream cap. Default 1 MiB, max 16 MiB.
env {string: string} Passed verbatim to children. Parent env is dropped.
auditLogPath string Optional. JSONL audit log path.
commands.<name> object See below.

Per-command rule:

Field Type Notes
subcommands string[] If set, args[0] must match one of these exactly.
argPattern regex string Every arg must match unless anyArgs: true.
anyArgs boolean Bypass argPattern (denyChars still applies). Use sparingly.
maxArgs integer Hard cap on args.length. Counts the subcommand too — tailscale up is 1, so set maxArgs: 1 (or omit).
denyChars string Override the default metacharacter denylist.

Run

node dist/index.js --config /abs/path/to/config.json

Wire into Claude Code

~/.claude/settings.json:

{
  "mcpServers": {
    "bash": {
      "command": "node",
      "args": ["/abs/path/bash-mcp/dist/index.js", "--config", "/abs/path/config.json"]
    }
  }
}

Tools exposed

  • run_command{ command, args?, cwd? } → stdout/stderr/exit code. cwd must be under the configured root.
  • list_allowed_commands — returns a human-readable dump of the allowlist and each rule.

Audit log

JSONL, one record per call:

{"ts":"2026-04-20T…","command":"git","args":["status"],"cwd":"/…","accepted":true,"exitCode":0,"durationMs":42,"stdoutPreview":""}
{"ts":"2026-04-20T…","command":"rm","args":["-rf","/"],"cwd":"/…","accepted":false,"rejectReason":"command \"rm\" is not in the allowlist"}

Rejected calls are logged too — review periodically to spot probing.

Development

npm test          # validator unit tests
npx tsc --noEmit  # type check
npm run dev -- --config examples/readonly-git.config.json

Validation logic lives in src/validator.ts and is pure — add a failing test there before changing behavior.

About

MCP server for running allowlisted bash commands — argv-only, no shell interpretation, per-command arg validation, cwd confinement, audit logging

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors