The shell
marsh opens a Linux shell with your project at its Mac path. msh is a symlink to the same program. After installing marsh, run this from your project directory:
marsh -c 'pwd; uname -sm'
The first line is your current Mac directory; the second is Linux aarch64.
The shell runs in a Docker Sandboxes (sbx) microVM:
| Invocation | Runs |
|---|---|
marsh | An interactive shell |
marsh -c 'make test' | One command string |
marsh ./script.sh arg | A script with arguments |
Registered agents work in pipelines and run in fresh containers:
claude -p 'list the TODOs in this repo' | sort | head
Where you are
- Project
- The directory you started in, mounted read/write at the same path.
pwdprints the Mac path. Changes appear on both sides immediately. marsh does not review or protect these files: any command you run, including an agent, can edit or delete them. - Home
$HOMEhas the same path as your Mac home. It is backed by~/.marsh/home(the guest home) and persists between shells, so put dotfiles there. SetMARSH_HOMEto use another scope (see Configuration).- User
- Your Mac user name and UID. The shell user has passwordless
sudoand a private Docker Engine (docker runworks) inside the VM. Packages installed withsudo apt-getlast as long as the VM. Kit jobs start as nonroot users in separate containers with no Docker or SBX sockets. See the security model for the shell VM's authority and explicit host grants. - System
- Debian on arm64 from the marsh shell image, with git, make, gcc, Node.js, Rust, Vim, and terminal Emacs.
Shared VM
Persistent-home shells in one scope share one warm shell VM and one daemon. A second project opened in the same scope can see the first project's files. To keep work apart, use a separate MARSH_HOME.
Ephemeral home
marsh --ephemeral-home starts a shell with a blank home that is not kept. That shell gets its own VM.
Command-line arguments
marsh passes options it does not recognize to the selected shell. For example, marsh -x script.sh enables tracing. In Brush, pipefail makes this command exit 1:
marsh -o pipefail -c 'false | true'
After -c or --, marsh passes every argument to the shell unchanged.
A script named like a marsh subcommand (stop, reset) must be run as marsh ./stop.
marsh commands in the shell
| Command | What it does |
|---|---|
split { a: ...; b: ... } | join [| CMD] | Branches on private copies; see Split and join |
fanout { a: ...; b: ... } | collect | Branches on the same files; see Fanout |
ps --marsh, top --marsh | This project's shell, jobs, and agent sessions |
acp run, acp ask, ... | Agent sessions over ACP; see ACP |
mcp publish, mcp load, ... | Publish a pipeline as an MCP tool; see MCP |
marsh jobs, marsh split, ... | The marsh CLI works inside the shell too |
split { and join directly after a split stage are marsh syntax. To reach the coreutils programs, use command split or command join.
Registered commands
Registered commands such as claude, codex, pi, and shell each run in a fresh container in a VM for that Kit. stdin, stdout, stderr, and exit status pass through. They work in pipelines, $( ), background jobs, scripts, and make.
Child processes get the same behavior from a private directory of links on PATH. type claude prints the link's path.
As in Bash, a function or builtin with the same name takes precedence. An explicit path bypasses both. See Commands and Kits.
What the shell is
The shell is Brush, a Bash-compatible shell written in Rust. marsh never runs Bash to implement itself.
The upstream record tracks the vendored revision and marsh's patch queue, including compatibility fixes and the composition syntax.
To use the VM's GNU Bash or zsh instead, run marsh --shell bash, set MARSH_SHELL=zsh, or set a default with marsh config shell zsh. Registered commands and the marsh CLI keep working. The split { } and fanout { } forms do not. See Choosing your shell.
Startup files
Edit ~/.bashrc at the marsh prompt (or ~/.marsh/home/.bashrc from your Mac in the default scope) to set aliases, environment variables, and your prompt. The home guide walks through setup. Your dotfiles survive VM removal. Packages you install in the VM do not.
Brush reads these startup files, with ~ referring to the guest home:
| Shell type | Files read |
|---|---|
| Interactive, non-login | /etc/bash.bashrc, ~/.bashrc, ~/.brushrc |
| Login | /etc/profile, then the first readable file among ~/.bash_profile, ~/.bash_login, ~/.profile |
| Noninteractive, non-login | The file named by $BASH_ENV, when set |
The prompt defaults to Bash's \s-\v\$ .
Editor settings
Vim and terminal Emacs are included: use vim FILE or emacs -nw FILE. They read ~/.vimrc and ~/.emacs.d/init.el from the guest home. From a Mac editor, those paths are ~/.marsh/home/.vimrc and ~/.marsh/home/.emacs.d/init.el in the default scope.
Bash compatibility
Brush supports ordinary Bash scripts and interactive use, with known gaps. These differences have regression tests:
- A
RETURNtrap does not fire. selectand${x~~}are not supported.(( m["a b"]=1 ))andm[a b]=1do not parse.- A trapped signal runs its trap after the current pipeline, not in the middle of a builtin. A blocking
readfrom a terminal is not interrupted. set -uon an unset variable exits with status 1.- Error messages use Brush's wording and add a
(col N)suffix. printf '%(fmt)T'uses the processTZ, not one set only in the shell.- A background job killed by a signal is reported as
Exit 143. Bash prints the signal name. - If your terminal never answers the cursor-position query, the line editor waits two seconds, warns, and falls back to plain line input.
The full list, with the test for each entry, is in Bash compatibility.
Report a difference
Open an issue with the smallest script that shows the difference and Bash's output. Fixes go into the Brush patch queue.
Next: Split and join, Choosing your shell.