memsh
Virtual bash shell in Go: executes shell commands against an in-memory filesystem (afero), with WASM plugin support and an embeddable HTTP server
About memsh
This large README is shown as a plain-text preview. Read the full README at the source
# memsh
[](https://github.com/amjadjibon/memsh/actions/workflows/test.yml)
[](https://pkg.go.dev/github.com/amjadjibon/memsh)
[](https://goreportcard.com/report/github.com/amjadjibon/memsh)
[](https://codecov.io/gh/amjadjibon/memsh)
A virtual bash shell implemented in Go. memsh executes bash-like commands against an in-memory filesystem — the real OS filesystem is never touched, and external OS commands are blocked by default.
Shell parsing and interpretation is handled by [mvdan.cc/sh/v3](https://github.com/mvdan/sh). Every command is a native Go plugin or WASM plugin — there is no reliance on host system binaries.
## Features
- **Sandboxed execution** — external OS commands are blocked; only registered plugins can run
- **In-memory filesystem** — all file operations target `afero.MemMapFs`; nothing touches your disk
- **Bash-like syntax** — pipes, redirects (`>`, `>>`), `&&`, `;`, subshells, aliases
- **60+ built-in commands** — file ops, text processing, archiving, networking, scripting, and more
- **Combined short flags** — `-rf`, `-la`, `-jrc` etc. work on all commands
- **Scripting languages** — Go (MVM interpreter), Lua (gopher-lua), JavaScript (goja ES2020+), JSON/YAML (jq/yq), SQLite
- **WASM plugin system** — extend with WASI-compiled plugins (Python, Ruby, PHP runtimes)
- **Native Go plugins** — register custom commands via a simple `Plugin` interface
- **Interactive REPL** — tab completion, command history, `.memshrc` startup script
- **HTTP server** — expose the shell over HTTP with session-scoped virtual filesystems
- **Network egress policy** — control outbound networking with domain/CIDR/port allowlists
- **Library usage** — embed memsh in Go programs for safe, sandboxed shell scripting
## Installation
### Homebrew
```bash
brew tap amjadjibon/memsh
brew install memsh
```
### Go Install
```bash
go install github.com/amjadjibon/memsh@latest
```
### Pre-built Binaries
Download pre-built binaries from the [GitHub Releases](https://github.com/amjadjibon/memsh/releases) page.
**Linux (amd64):**
```bash
curl -Lo memsh.tar.gz https://github.com/amjadjibon/memsh/releases/latest/download/memsh_linux_amd64.tar.gz
tar xzf memsh.tar.gz
sudo mv memsh /usr/local/bin/
```
**macOS (Apple Silicon):**
```bash
curl -Lo memsh.tar.gz https://github.com/amjadjibon/memsh/releases/latest/download/memsh_darwin_arm64.tar.gz
tar xzf memsh.tar.gz
sudo mv memsh /usr/local/bin/
```
**macOS (Intel):**
```bash
curl -Lo memsh.tar.gz https://github.com/amjadjibon/memsh/releases/latest/download/memsh_darwin_amd64.tar.gz
tar xzf memsh.tar.gz
sudo mv memsh /usr/local/bin/
```
**Windows (amd64):**
Download [`memsh_windows_amd64.zip`](https://github.com/amjadjibon/memsh/releases/latest/download/memsh_windows_amd64.zip), extract, and add `memsh.exe` to your `PATH`.
### Build from Source
```bash
git clone https://github.com/amjadjibon/memsh.git
cd memsh
go build -o memsh .
```
### Verify Installation
```bash
memsh --help
```
## Quick Start
```bash
# Interactive REPL
memsh
# Run a script
memsh ./path/to/script.sh
# Pipe commands
echo "mkdir /tmp && echo hello > /tmp/f && cat /tmp/f" | memsh
# HTTP server
memsh serve
memsh serve --addr :3000 --session-ttl 1h --cors-origin https://app.example.com
# Network-restricted shell
memsh --net-mode allowlist \
--net-allow-domain 'httpbin.org' \
--net-allow-port 443 \
-c 'curl https://httpbin.org/get'
```
## Usage as a Library
```go
package main
import (
"bytes"
"context"
"fmt"
"log"
"github.com/amjadjibon/memsh/pkg/shell"
)
func main() {
ctx := context.Background()
var out bytes.Buffer
sh, err := shell.New(shell.WithStdIO(nil, &out, &out))
if err != nil {
log.Fatal(err)
}
defer sh.Close()
err = sh.Run(ctx, `
mkdir -p /home/user/docs
echo '{"name":"alice","role":"admin"}' > /home/user/docs/user.json
jq -r .name /home/user/docs/user.json
`)
if err != nil {
log.Fatal(err)
}
fmt.Print(out.String()) // alice
}
```
### Pre-seeding the Virtual Filesystem
```go
fs := afero.NewMemMapFs()
afero.WriteFile(fs, "/config.yaml", []byte("host: localhost\nport: 8080\n"), 0644)
var out bytes.Buffer
sh, _ := shell.New(
shell.WithFS(fs),
shell.WithStdIO(nil, &out, &out),
)
sh.Run(ctx, "yq .host /config.yaml") // localhost
```
## Commands
| Command | Description |
| --- | --- |
| `cat` | Concatenate and print files |
| `cd` | Change working directory |
| `chmod` | Change file permissions (`-R` recursive) |
| `cp` | Copy files or directories (`-r`) |
| `cut` | Extract fields (`-f`) or characters (`-c`) |
| `date` | Print current date and time |
| `df` | Report filesystem disk space usage |
| `diff` | Compare two files line by line (`-u` unified) |
| `du` | Estimate file space usage |
| `echo` | Print arguments (`-n`, `-e`) |
| `env` | Print or set environment variables |
| `find` | Search virtual filesystem (`-name`, `-type`, `-maxdepth`) |
| `grep` | Search file contents (`-i`, `-n`, `-v`, `-r`, `-c`, `-l`, `-w`, `-o`) |
| `head` | Print first N lines (`-n`) or bytes (`-c`) |
| `ln` | Create hard or symbolic links (`-s`, `-f`) |
| `ls` | List directory contents (`-l`, `-a`, `-R`) |
| `mkdir` | Create directories (`-p`, `-v`, `-m`) |
| `mv` | Move or rename files |
| `printf` | Format and print data |
| `pwd` | Print working directory |
| `read` | Read a line from stdin into a variable |
| `rm` | Remove files or directories (`-f`, `-r`, `-v`) |
| `rmdir` | Remove empty directories |
| `sed` | Stream editor (substitution) |
| `seq` | Print a sequence of numbers |
| `sleep` | Delay for a specified amount of time |
| `sort` | Sort lines (`-r`, `-u`, `-n`) |
| `stat` | Show file status |
| `tail` | Print last N lines (`-n`) or bytes (`-c`) |
| `tee` | Read stdin; write to stdout and files (`-a`) |
| `timeout` | Run a command with a time limit |
| `touch` | Create or update file timestamps |
| `tr` | Translate or delete characters (`-d`, `-s`, `-c`) |
| `uniq` | Filter adjacent duplicate lines (`-c`, `-d`, `-u`) |
| `wc` | Count lines, words, and bytes (`-l`, `-w`, `-c`) |
| `which` | Locate a command |
| `xargs` | Build and execute command lines from stdin |
| `yes` | Repeatedly output a string |
| `awk` | Pattern scanning and processing |
| `base64` | Encode or decode base64 (`-d`) |
| `bc`, `expr` | Arbitrary precision calculator / expression evaluator |
| `column` | Columnate output |
| `crontab` | Schedule commands with cron expressions |
| `curl` | Transfer data from URLs |
| `envsubst` | Substitute environment variables in strings |
| `go` | Go tool — `go run`, `go test`, `go fmt` against the virtual filesystem |
| `goja` | Execute JavaScript (ES2020+) code |
| `git` | Pure-Go git implementation |
| `gzip`, `gunzip` | Compress/decompress gzip files |
| `hexdump`, `xxd` | Hex dump of files |
| `jq` | Command-line JSON processor |
| `less`, `more` | Scrollable pager (web terminal) |
| `ln` | Create links |
| `lua` | Execute Lua 5.1 code |
| `man`, `help` | Show help for commands |
| `md5sum`, `sha256sum`, … | File checksum (md5, sha1, sha224, sha256, sha384, sha512) |
| `mktemp` | Create a temporary file or directory |
| `sqlite3` | SQLite database shell |
| `ssh` | Connect to a remote memsh server |
| `tar` | Archive files |
| `tree` | Display directory contents as an ASCII tree (`-L`, `-a`, `-d`, `-f`) |
| `tput`, `stty` | Terminal control stubs |
| `yq` | Command-line YAML/JSON processor |
| `zip`, `unzip` | Compress/decompress zip files |
## Plugin System
### Native Go Plugins
Implement the `Plugin` interface from `pkg/shell/plugins`:
```go
import (
"github.com/amjadjibon/memsh/pkg/shell/plugins"
"mvdan.cc/sh/v3/interp"
)
type HelloPlugin struct{}
func (HelloPlugin) Name() string { return "hello" }
func (HelloPlugin) Description() string { return "greet the user" }
func (HelloPlugin) Usage() string { return "hello [name]" }
func (HelloPlugin) Run(ctx context.Context, args []string) error {
hc := interp.HandlerCtx(ctx) // pipe-aware I/O — always use this
sc := plugins.ShellCtx(ctx) // virtual FS, cwd, ResolvePath, SetEnv, …
fmt.Fprintf(hc.Stdout, "Hello from %s!\n", sc.Cwd)
return nil
}
```
Register at shell creation time:
```go
sh, _ := shell.New(shell.WithPlugin(HelloPlugin{}))
```
Or add to `defaultNativePlugins()` in `pkg/shell/defaults.go` to include it in every shell instance.
### JSON Processing (`jq`)
```bash
echo '{"name":"alice","scores":[10,20,30]}' | jq .name # "alice"
echo '{"name":"alice"}' | jq -r .name # alice (no quotes)
echo '{"items":[1,2,3]}' | jq '.items | length' # 3
jq -n '{generated: true}' # null input
jq -rc .name data.json # combined flags
```
### YAML/JSON Processing (`yq`)
```bash
echo 'name: alice' | yq .name # alice
echo 'name: alice' | yq -j . # JSON output
printf 'items:\n - a\n - b\n' | yq '.items[0]' # a
yq .host /config.yaml # read from virtual FS
yq -jc . data.yaml # compact JSON output
```
### Lua Scripting
```bash
lua -e 'print("hello from lua")'
echo 'for i=1,3 do print(i) end' | lua
lua /script.lua
```
### Go Scripting (`go`)
The `go` command emulates the Go toolchain against the virtual filesystem, backed by the [MVM interpreter](https://github.com/mvm-sh/mvm). stdlib is auto-imported — no `import` statements needed for inline expressions.
```bash
# go run — execute a source file
echo 'package main' > /main.go
echo 'func main() { fmt.Println("hello") }' >> /main.go
go run /main.go # hello
# go run — fibonacci
echo 'package main' > /fib.go
echo 'func fib(n int) int { if n<=1{return n}; return fib(n-1)+fib(n-2) }' >> /fib.go
echo 'func main() { for i:=0;i<=7;i++ { fmt.Println(i,fib(i)) } }' >> /fib.go
go run /fib.go
# go test — runs Test* functions; reports PASS/FAIL per test
echo 'package main' > /math_test.go
echo 'import "testing"' >> /math_test.go
echo 'func TestAdd(t *testing.T) { if 1+1!=2 { t.Error("broken") } }' >> /math_test.go
go test / # --- PASS: TestAdd / ok
go test ./... # recurse all subdirs
go test / -run TestAdd # filter by name regex
go test / -v # verbose (=== RUN lines)
# go fmt — gofmt source files in the virtual FS
echo 'package main' > /ugly.go
echo 'func main(){fmt.Println("hi")}' >> /ugly.go
go fmt /ugly.go
cat /ugly.go # properly formatted
# stdin pipe — auto-imported stdlib, no package/import needed
echo 'fmt.Println(strings.ToUpper("hello"))' | go
echo 'fmt.Println(math.Sqrt(144))' | go
# go version
go version # go version mvm0.3.0
```
**Notes:**
- `go test` rewrites `*testing.T` to a built-in shim; `t.Error`, `t.Errorf`, `t.Fatal`, `t.Fatalf`, `t.Log`, `t.Run`, `t.Skip` all work.
- MVM is alpha (v0.3.0); some stdlib packages are partially supported. Unsupported calls surface as interpreter errors.
- stdin mode and `go run` use stdlib auto-import — `fmt`, `strings`, `math`, etc. work without explicit imports.
### JavaScript Scripting
```bash
goja -e 'console.log("hello")'
echo 'console.log("test")' | goja
goja /script.js
goja -e 'const arr=[1,2,3]; console.log(arr.map(x=>x*2).join(","))'
```
### WASM Plugins
WASM plugins are compiled with `GOOS=wasip1 GOARCH=wasm`. The virtual FS is mounted via WASI so file I/O goes directly into `afero.MemMapFs`.
```bash
go run . plugin install python # Python 3.12.0 (~25 MB)
go run . plugin install ruby # Ruby 3.2.2 slim (~8 MB)
go run . plugin install php # PHP 8.2.6 slim (~6 MB)
go run . plugin install /path/to/plugin.wasm # local file
go run . plugin list # list installed plugins
```
Installed runtimes are stored in `~/.memsh/plugins/*.wasm`.
### Plugin Loading Priority
1. `WithPlugin(p)` or `WithPluginBytes(name, wasm)` options
2. Native Go plugins registered in `defaultNativePlugins()`
3. Embedded WASM from `defaultPlugins` map (currently empty)
4. `/memsh/plugins/*.wasm` in the virtual FS
5. `~/.memsh/plugins/*.wasm` on the real OS filesystem
## Options
| Option | Description |
| --- | --- |
| `WithFS(fs)` | Set the afero filesystem (default: `afero.NewMemMapFs()`) |
| `WithCwd(path)` | Set initial working directory |
| `WithEnv(env)` | Set initial environment variables |
| `WithStdIO(in, out, err)` | Set standard I/O streams |
| `WithPlugin(p)` | Register a native plugin |
| `WithBuiltin(name, fn)` | Register a raw function as a command |
| `WithPluginBytes(name, wasm)` | Register a WASM plugin from bytes |
| `WithWASMEnabled(bool)` | Enable/disable WASM runtime (default: true) |
| `WithPluginFilter(names)` | Allowlist for WASM plugin discovery |
| `WithDisabledPlugins(names...)` | Exclude specific plugins by name |
| `WithAllowExternalCommands(bool)` | Allow falling back to real OS executables (default: false) |
| `WithInheritEnv(bool)` | Inherit parent process environment (default: true; use false in server mode) |
| `WithAliases(map)` | Pre-seed the alias table |
| `WithNetworkPolicy(policy)` | Set outbound network policy (`off`, `allowlist`, `full`) |
| `WithNetworkLimits(limits)` | Set network request/bytes/runtime limits |
| `WithNetworkUsage(usage)` | Seed cumulative network usage (for restored sessions) |
## HTTP Server
```bash
go run . serve # listen on :8080
go run . serve --addr :3000 --cors-origin https://app.example.com
go run . serve --session-ttl 1h --timeout 30s
```
Sessions are always enabled. Send `X-Session-ID: <id>` on `POST /run` to persist the virtual filesystem across requests.
| Endpoint | Description |
| --- | --- |
| `GET /` | Web terminal UI |
| `POST /run` | `{"script":"..."}` → `{"output":"...","cwd":"...","error":"..."}` |
| `GET /sessions` | List active sessions (cwd, timestamps, runtime/network usage counters) |
| `DELETE /session/{id}` | Destroy a session |
| `GET /health` | `{"status":"ok","uptime":"...","sessions":N}` |
| `POST /complete` | `{"input":"...","cursor":N}` → tab completion |
| `GET /session/{id}/snapshot` | Export session filesystem as JSON |
| `POST /session/{id}/snapshot` | Import a snapshot (use `"new"` as id to create) |
### Networking Policy Flags
These flags work for both local `memsh` and `memsh serve`:
```bash
--net-mode off|allowlist|full
--net-allow-domain <domain> # repeatable, supports *.example.com
--net-allow-cidr <cidr> # repeatable, e.g. 203.0.113.0/24
--net-allow-port <port> # repeatable, e.g. 443
--net-max-requests <n> # 0 = unlimited
--net-max-bytes-sent <n> # 0 = unlimited
--net-max-bytes-recv <n> # 0 = unlimited
--net-max-runtime <duration> # 0 = unlimited, e.g. 30s
```
Examples:
```bash
# Block all outbound networking
memsh --net-mode off -c 'curl https://example.com'
# Allow only HTTPS to httpbin.org
memsh --net-mode allowlist \
--net-allow-domain 'httpbin.org' \
--net-allow-port 443 \
-c 'curl https://httpbin.org/get'
```
If DNS fails (`lookup <host>: no such host`), that is environment/network resolution, not a policy deny. A policy deny returns explicit errors like `network disabled by policy` or `destination port ... is not allowed`.
`GET /sessions` now includes usage counters:
```json
{
"id": "abc123",
"cwd": "/",
"created_at": "2026-04-17T08:00:00Z",
"last_use": "2026-04-17T08:01:00Z",
"runtime_ms": 1420,
"network_requests": 3,
"network_bytes_sent": 512,
"network_bytes_received": 4096,
"network_runtime_ms": 280
}
```
## LLM Integration
memsh has two modes for connecting LLMs: an **MCP server** (any MCP-compatible client) and a built-in **agent** (interactive ReAct loop).
### MCP Server (`memsh mcp`)
The MCP server exposes a single `memsh` tool that lets any LLM execute bash commands in a sandboxed in-memory filesystem. The real OS is never touched.
**Transports:**
| Transport | Command | Use case |
|---|---|---|
| stdio (default) | `memsh mcp` | Claude Desktop, Claude Code CLI |
| HTTP (MCP 2025-03-26+) | `memsh mcp --transport http --addr :8080` | Programmatic / multi-session |
| SSE (legacy) | `memsh mcp --transport sse --addr :8080` | Legacy MCP clients |
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"memsh": {
"command": "/usr/local/bin/memsh",
"args": ["mcp"]
}
}
}
```
**Claude Code CLI:**
```bash
claude mcp add memsh -- memsh mcp
```
**Other MCP clients** — start the HTTP transport and point your client at the endpoint:
```bash
memsh mcp --transport http --addr :8080
# Connect to: http://localhost:8080/
```
**Tool behaviour:**
- Tool name: `memsh`, input field: `command` (string)
- The virtual filesystem **persists across calls** within a session — use it as a scratchpad
- `exit` / `quit` are treated as success, not errors
- Stdin is not available; commands that read stdin receive EOF
- Returns command output + current working directory (`Cwd: /path`)
- Per-call timeout (default 30 s, minimum 5 s): `memsh mcp --timeout 1m`
- WASM plugins (Python/Ruby/PHP) disabled by default for fast startup: `memsh mcp --wasm`
**Example tool call result:**
```
/home/user/data
file1.txt file2.txt
Cwd: /home/user/data
```
## Configuration
`~/.memsh/config.toml` is loaded at startup (missing file = defaults):
```toml
[shell]
wasm = true # set false to skip all WASM loading (faster startup)
[plugins]
wasm = ["python"] # allowlist of ~/.memsh/plugins/*.wasm names; empty = load all
disable = ["wc"] # exclude specific plugins by name (native or WASM)
```
Configuration files:
- `~/.memsh/config.toml` — shell and plugin configuration
- `~/.memsh/.memshrc` — startup script (sourced at REPL start and first HTTP session)
- `~/.memsh/history/` — per-session command history
- `~/.memsh/plugins/` — user-installed WASM plugins
## Testing
```bash
go test ./... # full test suite
go test ./tests -v # integration tests verbose
go test ./tests -run TestJq -v # single suite
go test ./pkg/shell/... -run TestName # shell package tests
```
## Development
```bash
# Build
make build
# Run tests
make test
# Run coverage report
make cover
# Lint
make lint
# Clean build artifacts
make clean
# View all available commands
make help
```
### Creating a Release
The project uses [GoReleaser](https://goreleaser.com/) for automated releases and Homebrew cask generation.
```bash
# 1. Test the release process (dry-run)
make release-dry-run TAG=v1.0.0
# 2. Create the actual release
make release TAG=v1.0.0
```
The `make release` command will:
1. **Commit and push any uncommitted changes** (prepares for release)
2. **Clean the `dist/` directory** (removes old build artifacts)
3. Clean build artifacts (bin/ and *.wasm files)
4. Create and push a git tag
5. Build binaries for all platforms (Linux, macOS, Windows × AMD64, ARM64)
6. Create a GitHub Release with all binaries
7. **Generate and push the Homebrew cask** automatically via goreleaser to [`homebrew-memsh`](https://github.com/amjadjibon/homebrew-memsh)
After release, users can install via:
```bash
brew tap amjadjibon/memsh
brew install memsh
```
Frequently Asked Questions
What is memsh?
memsh is a Command Line library for the Go programming language. Virtual bash shell in Go: executes shell commands against an in-memory filesystem (afero), with WASM plugin support and an embeddable HTTP server
How do I install memsh?
Install memsh with the Go module system using `go get amjadjibon/memsh`. Check the repository for the current installation instructions.
What category does memsh belong to?
memsh is listed under Command Line, specifically Standard CLI.