Shell and coding agent MCP server (fork of wcgw) with per-chat isolation
  • Python 98.7%
  • Dockerfile 1%
  • Makefile 0.3%
Find a file
airaneel 7996fc9ac7
All checks were successful
build-image / build (push) Successful in 29s
refactor: name who may use this server, instead of selling them a licence
The gate this replaces was borrowed from the paid MCPs in this fleet, where a
per-service licence is what a stranger buys. babash sells nothing and has one
user, so the mechanism came out as a purchase ceremony its owner had to perform
on himself: mint a key, then paste it back into a login form, to be granted
access to his own shell. That is not a security control, it is paperwork.

The reason a second gate exists at all is unchanged and still real. The scope
is what a client asks for, and mcp-auth hands one to anyone who can log in —
its Nextcloud SSO has no signup restriction, no allowlist and no domain filter,
so "holds a babash scope" is not far from "has an account". A root shell needs
to know who is holding the token, not merely what they asked for.

BABASH_ALLOWED_SUBJECTS says that directly: the issuer user ids that may open a
shell. No licence store to be reachable, no key to mint, no form to fill in, and
revoking someone is deleting them from a list rather than writing a row that
means they stopped paying.

Empty allows nobody. For a root shell that is the only safe reading of a missing
config — it has to lock us out rather than let everyone in — but the failure is
otherwise indistinguishable from a broken issuer (every login succeeds, every
call still 401s), so it is announced loudly at startup and each refusal logs the
id that was turned away. That line is also how the list gets filled the first
time: connect once, read who was refused, and if it is you, add it. A user id is
not a credential, so printing it costs nothing.

Verified live: with the list empty the server warns at startup and refuses, and
a bogus bearer still gets 401 after a real introspection call. 118 tests pass,
mypy --strict and ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 15:14:18 +03:00
.forgejo/workflows ci: build on the beelink runner; let the package hook deploy 2026-07-18 13:11:15 +03:00
.vscode fix: capture_output on screen/opener subprocesses to keep stdout clean 2026-07-12 01:16:12 +03:00
docker build: ssh config as a file, and stop forging a git identity 2026-08-03 01:30:40 +03:00
src/babash refactor: name who may use this server, instead of selling them a licence 2026-08-16 15:14:18 +03:00
static Updated demo gif 2025-03-16 15:28:14 +05:30
tests refactor: name who may use this server, instead of selling them a licence 2026-08-16 15:14:18 +03:00
.env.example feat: add Makefile for docker build and push 2026-03-19 03:56:22 +03:00
.gitignore fix: remove orphaned tools.py, fix unused imports in tests 2026-03-21 15:06:02 +03:00
.gitmodules Increased context length and migrated to official mcp sdk 2025-05-03 22:22:06 +05:30
.prettierignore fix: allow empty send_input to press Enter 2026-03-21 23:14:30 +03:00
.prettierrc fix: allow empty send_input to press Enter 2026-03-21 23:14:30 +03:00
.python-version Updated workflow and fixed Python compatibility for tests 2024-10-23 00:28:03 +05:30
CLAUDE.md refactor: make babash a terminal MCP server and nothing else 2026-07-14 20:03:02 +03:00
docker-compose.portainer.yml fix: allow empty send_input to press Enter 2026-03-21 23:14:30 +03:00
docker-compose.yml fix: allow empty send_input to press Enter 2026-03-21 23:14:30 +03:00
Dockerfile build: ssh config as a file, and stop forging a git identity 2026-08-03 01:30:40 +03:00
LICENSE Added more tests 2024-12-25 13:24:55 +05:30
Makefile build: push to the Forgejo registry instead of Docker Hub 2026-07-16 12:42:52 +03:00
pyproject.toml refactor: move to fastmcp, off the SDK's own FastMCP 2026-08-08 03:28:10 +03:00
README.md feat: per-chat shell isolation via model-supplied chat_id 2026-07-13 15:59:52 +03:00
stack.env fix: allow empty send_input to press Enter 2026-03-21 23:14:30 +03:00
uv.lock refactor: move to fastmcp, off the SDK's own FastMCP 2026-08-08 03:28:10 +03:00

babash

Shell and coding agent MCP server. Fork of wcgw, modernized with FastMCP and decomposed architecture.

Features

  • Interactive shell — fully interactive terminal via pexpect/pyte, supporting arrow keys, Ctrl-C, background processes
  • Smart file editing — search/replace with fuzzy matching, indentation tolerance, syntax checking on writes
  • File protections — read-before-edit enforcement, hash-based change detection, token-aware truncation with temp file save
  • Three modes — full access (default), architect (read-only), code_writer (restricted paths/commands)
  • Task persistence — save/resume context across chat sessions via ContextSave
  • ML file ranking — pre-trained model scores file importance for smart repo overviews
  • Streamable HTTP — supports both stdio and streamable-http transports

Setup

Claude Desktop

Install uv, then add to claude_desktop_config.json:

{
  "mcpServers": {
    "babash": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/airaneel/babash", "babash"]
    }
  }
}

To force a specific shell:

{
  "mcpServers": {
    "babash": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/airaneel/babash", "babash", "--shell", "/bin/zsh"]
    }
  }
}

Streamable HTTP (remote deployment)

babash_mcp --transport streamable-http

Docker

docker build -t babash https://github.com/airaneel/babash.git
docker run -i --rm --mount type=bind,src=/your/workspace,dst=/workspace babash

Per-chat isolation (chat_id)

A single babash process is shared by every conversation that connects to it (over stdio all of Claude Desktop's chats share one process; over streamable-http the client sends no per-conversation id either — claude-code#41836). So the transport gives the server no way to tell chats apart.

babash resolves this with a chat_id the model carries in its own context: babash_initialize mints one and every other tool requires it. Each chat_id owns an independent "main" shell plus its own named sessions — separate cwd, env, running command and on-disk state — so parallel chats never step on each other, with no locking. An unknown chat_id is rejected rather than silently sharing another chat's shell. If the model omits the id, isolation degrades but nothing is corrupted (collision-free shell ids keep state files and screen sessions distinct regardless).

Tools

Tool Description
Initialize Set up workspace, mode, resume tasks
BashCommand Execute shell commands, check status, send keystrokes
ReadFiles Read files with optional line ranges (file.py:10-20)
ReadImage Read image files
FileWriteOrEdit Write new files or edit existing ones (auto-selects mode by % changed)
ContextSave Save task context + relevant files for later resumption

Modes

Mode Shell File Edit File Write
default Full access All files All files
architect Read-only None None
code_writer Configurable Specified globs Specified globs

Terminal attachment

If screen is installed, babash runs in a screen session. Attach with:

screen -ls          # find the session
screen -x <id>      # attach (use Ctrl+A+D to detach)

Architecture

src/babash/
├── __init__.py                     # Entry point
├── types_.py                       # Pydantic models for all tool inputs
└── client/
    ├── mcp_server/
    │   ├── __init__.py             # Typer CLI (stdio + streamable-http)
    │   └── server.py              # FastMCP server with lifespan
    ├── bash_state/
    │   ├── bash_state.py          # Core BashState class
    │   ├── shell_process.py       # pexpect/pyte, screen sessions
    │   ├── execute.py             # Command execution engine
    │   ├── file_whitelist.py      # Read-before-edit tracking
    │   └── persistence.py         # State serialization to disk
    ├── file_ops/
    │   ├── search_replace.py      # Aider-style search/replace
    │   ├── diff_edit.py           # Fuzzy matching engine
    │   └── extensions.py          # Language detection, token limits
    ├── repo_ops/                  # Git-aware repo analysis, ML ranking
    ├── tools.py                   # Tool dispatch and file operations
    ├── modes.py                   # Mode definitions and prompts
    ├── tool_prompts.py            # MCP tool schemas and descriptions
    └── encoder/                   # Lazy-loaded tokenizer

Development

uv sync
uv run mypy --strict src/babash    # type checking
uv run pytest                       # tests

Credits

Fork of rusiaaman/wcgw. Modernized with FastMCP, decomposed BashState, cleaned up types.