- Python 98.7%
- Dockerfile 1%
- Makefile 0.3%
|
All checks were successful
build-image / build (push) Successful in 29s
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> |
||
|---|---|---|
| .forgejo/workflows | ||
| .vscode | ||
| docker | ||
| src/babash | ||
| static | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| .gitmodules | ||
| .prettierignore | ||
| .prettierrc | ||
| .python-version | ||
| CLAUDE.md | ||
| docker-compose.portainer.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| stack.env | ||
| uv.lock | ||
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.