# marsh manual > marsh is a Bash-compatible Linux shell for Apple Silicon Macs. Agent commands run in fresh containers inside Docker Sandbox VMs. Use `split { } | join` for private project copies, or `fanout { } | collect` for shared files. Inside jobs, use the `marsh split` and `marsh fanout` CLI forms; registered agent calls start child jobs. marsh is a shell for composing agents on Apple Silicon Macs. It opens a Linux shell at the project's Mac path. Registered commands such as `claude`, `codex`, and `pi` each run in a fresh nonroot container inside a warm VM for that Kit. `msh` is the same program. ## Install You need an Apple Silicon Mac (`uname -m` prints `arm64`), Homebrew, and Docker Sandboxes (`sbx`) signed in. See https://runmar.sh/docs/install.md for the supported `sbx` version and other installation methods. Docker Desktop is not required to run marsh. Intel Macs, Linux, and Windows are not supported hosts. ```sh brew install docker/tap/sbx mcavage/tap/marsh sbx login # interactive sign-in sbx secret set anthropic # interactive key entry; claude and pi sbx secret set openai # interactive key entry; codex marsh --version ``` The `sbx` sign-in and key-entry steps need an interactive terminal. `sbx secret ls` lists stored credentials; marsh has no login of its own. Provider keys managed by the `sbx` proxy stay outside jobs. An agent's own login flow can save credentials in the shared guest home, where other jobs can read them. ## Run Use `marsh` for an interactive shell or `marsh -c 'COMMAND'` when your tool cannot drive a terminal. The default shell is Brush, with Bash-compatible syntax and the gaps listed at https://runmar.sh/docs/shell.md#bash-compatibility. `--shell bash` and `--shell zsh` select those shells inside the VM instead. ```sh cd ~/src/project marsh -c 'pwd; uname -sm' marsh -c 'claude -p "summarize README.md" # marsh manual marsh is a shell for composing agents on Apple Silicon Macs. It opens your project in Linux at its Mac path, runs registered agent commands in fresh containers, and lets you combine their work with shell syntax. ## Getting started - [Install](https://runmar.sh/docs/install.md): requirements, installation methods, upgrades, removal. - [Quickstart](https://runmar.sh/docs/quickstart.md): one agent job on a project copy to find the repository's test command and verify its source. ## Guides - [Make independent changes in parallel](https://runmar.sh/docs/guides/race-two-agents.md): give each task a project copy and bring the patches back yourself. - [Review a change with a panel](https://runmar.sh/docs/guides/review-with-a-panel.md): ask two reviewers different questions about the same diff. - [Let an agent farm out work](https://runmar.sh/docs/guides/farm-out-work.md): have an agent start one bounded child job from its shell tool. - [Review split results in a pipeline](https://runmar.sh/docs/guides/add-a-judge.md): send results to another agent for an optional second opinion. - [Set up your marsh home](https://runmar.sh/docs/guides/set-up-your-home.md): skills, dotfiles, shared and per-Kit MCP servers and extra mounts. ## Using marsh - [The shell](https://runmar.sh/docs/shell.md): guest paths, startup files, Bash compatibility. - [Using bash or zsh instead](https://runmar.sh/docs/shells.md): shell selection and startup files. - [Commands and Kits](https://runmar.sh/docs/kits.md): packaged commands and your own registrations. - [Agents running agents](https://runmar.sh/docs/agents.md): child jobs, launch limits, cancellation. - [Split and join](https://runmar.sh/docs/split.md): project copies and patch handling. - [Fanout and collect](https://runmar.sh/docs/fanout.md): parallel commands on shared files. - [Docker Cloud (preview)](https://runmar.sh/docs/cloud.md): `cloud { ... }` runs a block in a cloud sandbox and writes its file changes back. - [Agent sessions (ACP)](https://runmar.sh/docs/acp.md): long-lived sessions for ACP clients. - [MCP](https://runmar.sh/docs/mcp.md): server setup and published pipelines. ## Reference - [Configuration](https://runmar.sh/docs/configuration.md): settings and paths. - [Troubleshooting](https://runmar.sh/docs/troubleshooting.md): refused jobs, stuck VMs, cleanup. - [Security model](https://runmar.sh/docs/security.md): process authority and isolation boundaries. - [FAQ](https://runmar.sh/docs/faq.md): costs, authentication, comparisons with other tools. - `man marsh`: command options ([source](https://runmar.sh/docs/marsh.1.md)). ## Working on marsh - [CONTRIBUTING.md](https://github.com/mcavage/marsh/blob/main/CONTRIBUTING.md): build and test loop. - [Design documents](https://runmar.sh/docs/design/index.md): architecture, contracts, TLA+ model. - [GitHub Discussions](https://github.com/mcavage/marsh/discussions): Kits and recipes from other users. --- # Install Install marsh and Docker Sandboxes on your Mac, then open your project in a Linux shell. The [Quickstart](https://runmar.sh/docs/quickstart.md) covers provider credentials and your first agent calls. ## Requirements You need an **Apple Silicon Mac** and Docker Sandboxes (`sbx`) **0.47.0 or newer**, signed in. Check the installed version with `sbx version`. It does not run on Intel Macs or Linux hosts. Docker Desktop is not required for a release install. You need Desktop with Buildx only when building marsh from source or running a Kit from a local source directory. Release binaries are unsigned and not notarized. ## Install marsh ### Homebrew In your **Mac terminal**: ```sh brew install docker/tap/sbx mcavage/tap/marsh sbx login ``` This installs `sbx` as a cask and marsh as a formula. The marsh formula cannot depend on that cask, so the command names both. If you install only marsh, it will tell you to install `sbx` before starting. The formula puts `marsh`, `msh`, `marshd`, and `marsh-mcp` on your `PATH`, installs guest binaries under its `libexec`, and includes the `marsh(1)` and `msh(1)` man pages. ### curl To install marsh with the release script, first install and sign in to Docker Sandboxes. These commands still use Homebrew for `sbx`: ```sh brew install docker/tap/sbx sbx login curl -fsSL https://runmar.sh/install | sh ``` The script installs into `~/.local`. It checks the host architecture and `sbx` version, downloads the latest GitHub release, and verifies its SHA-256 checksum. It requires `curl`, `tar`, and `shasum`. If `sbx` is absent, the script warns but installs marsh; you must install `sbx` before marsh can run. The checksum catches corrupt downloads, not a compromised release. The install contains: ```text ~/.local/bin/marsh, msh, marshd, marsh-mcp ~/.local/libexec/marsh/ guest binaries, command registry, shell image reference ~/.local/share/man/man1/marsh.1, msh.1 ~/.local/share/licenses/marsh/ ``` The script does not edit your shell profile. If `~/.local/bin` is not on your `PATH`, add it to the profile for your Mac shell. For zsh: ```sh echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc exec zsh ``` To use another install prefix: ```sh curl -fsSL https://runmar.sh/install | sh -s -- --prefix /usr/local ``` `--prefix` must be absolute. If the directory is not writable, the script asks before using `sudo`. Without a terminal it exits unless you pass `--yes`. Use `--version VERSION` to pin a release. `MARSH_PREFIX` and `MARSH_VERSION` set the same values as those flags. If you want to read the installer first: ```sh curl -fsSL https://runmar.sh/install -o install.sh less install.sh sh install.sh ``` ### Release tarball Download `marsh-VERSION-darwin-arm64.tar.gz` and its `.sha256` file from [GitHub Releases](https://github.com/mcavage/marsh/releases). Substitute your chosen version and run `shasum -a 256 -c marsh-VERSION-darwin-arm64.tar.gz.sha256` in the download directory. Extract the archive and copy its `bin/`, `libexec/`, and `share/` directories into your chosen prefix. Keep `bin/` and `libexec/marsh/` under the same prefix. The host executable looks for guest binaries in `../libexec/marsh`. ## Check the install In your **Mac terminal**, change to an existing project directory, then run: ```sh marsh --version marsh -c 'pwd; uname -sm' ``` The first command prints the installed version. The second starts a noninteractive guest shell and prints your project's absolute Mac path, followed by `Linux aarch64`. It returns you to the Mac terminal when done. The first launch can take a few minutes while Docker Sandboxes prepares the shell VM. Later launches reuse that VM. Agent calls need a provider credential as well as `sbx login`. Follow the [Quickstart](https://runmar.sh/docs/quickstart.md) before making your first call. ## Upgrade Upgrade marsh with Homebrew; Kits update themselves. ```sh brew upgrade mcavage/tap/marsh marsh stop # when nothing is running; the next launch uses the new marsh ``` The commands (`claude`, `codex`, `pi`, `shell`, `pix`, `hermes`, and Kits you installed from a tag) move to newer images by themselves: marsh checks each time its daemon starts and once an hour, and switches a command when it has no running jobs. The next time you use an updated command, marsh prints one line such as `marsh: claude updated (2.1.305 → 2.1.307)`. `marsh status` lists recent updates. To update right now, run `marsh stop` when nothing is running, then use marsh again. To turn updates off, run `marsh config auto-update off`. See [Updates](https://runmar.sh/docs/kits.md#updates). A command that was updated keeps its newer image when marsh itself is upgraded. `marsh kit uninstall NAME` returns a packaged command to the version your marsh shipped. For a script install, run the installer again instead of `brew upgrade`. For a tarball install, repeat the extraction and copy steps with the new release. A running daemon keeps the old marsh until `marsh stop`: finish its jobs, `exit` every marsh shell, then run `marsh stop` from your **Mac terminal**. Stop refuses while shells or jobs are active; see [Removing VMs](https://runmar.sh/docs/troubleshooting.md#removing-vms). After an `sbx` upgrade, marsh replaces its daemon automatically once no shell or job is running. In projects where you registered an MCP client, rerun `marsh mcp install CLIENT` with that client's name because the registration stores the `sbx` path. ## Uninstall Finish jobs and leave every marsh shell with `exit`. In your **Mac terminal**, remove marsh's VMs and stop its daemon: ```sh marsh stop ``` For Homebrew: ```sh brew uninstall marsh ``` For a default `~/.local` script or tarball install, remove its installed files. Adjust these paths if you used another prefix: ```sh rm -f ~/.local/bin/{marsh,msh,marshd,marsh-mcp} ~/.local/share/man/man1/{marsh,msh}.1 rm -rf ~/.local/libexec/marsh ~/.local/share/licenses/marsh ``` These commands leave user data in place. Remove any of the following only if you no longer need its contents: | Path | Contents | | --- | --- | | `~/.marsh` | Guest home, including saved agent logins and history | | `~/Library/Application Support/marsh` | Host control data and job records | | `~/Library/Application Support/marsh-mcp-scopes` | Generated MCP server scopes | | `~/Library/Caches/marsh` | Cached Kit images | ## Docker Sandboxes policy The release shell image and Kits are published as `docker.io/mcavage/marsh-*`, with version tags and `:latest`. If your Docker Sandboxes policy restricts Kit sources, allow `docker.io/mcavage`. ## From source You need Docker Desktop with Buildx, Rust 1.99, Python 3.10 or newer, `jq`, and the `sbx` version listed under [Requirements](#requirements). See [CONTRIBUTING.md](https://github.com/mcavage/marsh/blob/main/CONTRIBUTING.md) for the development loop. ```sh git clone https://github.com/mcavage/marsh cd marsh make install ``` `make install` builds and installs into `~/.local`. To package a local build as a tarball instead: ```sh make dist-local ``` The archive is `target/dist/marsh-VERSION-local-darwin-arm64.tar.gz`. Extract that one archive into an empty directory and copy its `bin/`, `libexec/`, and `share/` into your prefix, as for a release tarball. A local archive includes `LOCAL-BUILD.txt`, and `marsh --version` identifies it as a local build. Its Kits are unpinned source directories, built on first use, and its shell image lives in that Mac's `sbx` image store. Install it only on the Mac that built it. The release target, `make dist`, refuses unpinned Kits and directs you to `make dist-local`. Next, follow the [Quickstart](https://runmar.sh/docs/quickstart.md), or consult [Troubleshooting](https://runmar.sh/docs/troubleshooting.md) if the install does not start. --- # Quickstart Ask an agent to find your repository's test command and cite where it is defined. The example uses one job on a private project copy and asks only for a report. Use an existing Git repository with a tracked `README.md`, opened at its root. Split needs a checkout with a `.git` directory; linked worktrees and submodule roots with a `.git` file are refused. To run the tests afterward, you will need the project's Linux arm64 dependencies. ## Install and sign in You need an **Apple Silicon Mac** and **Docker Sandboxes** (`sbx`) **0.47.0 or newer**. In your **Mac terminal**, install both tools, sign in to Docker Sandboxes, and configure the provider used below: ```sh brew install docker/tap/sbx mcavage/tap/marsh sbx login sbx secret set anthropic ``` The last command configures Anthropic for the Claude job below. [Install](https://runmar.sh/docs/install.md) has other installation methods. In the same Mac terminal, change to your repository's root and run: ```sh marsh ``` You are now in Linux at the same project path. The first shell launch may download its VM template. First use of Claude also starts its Kit VM and may download an image of about 600 MB. Allow a few minutes for these cold starts; later launches reuse warm VMs. ## Split a task At the **marsh prompt**: ```sh split { tests: claude -p 'Find the command this repository uses to run its tests. Start with README.md, then check the build files and CI configuration. Cite the defining file and line, and explain any required setup. If there is no test command, say what you checked. Do not edit files, run tests, or start other agents.' } | join --keep ``` `split` gives the job a project copy, called a *fork*. Its container mounts that fork rather than your original project. The copy includes uncommitted work but omits Git-ignored files. Copy-on-write cloning shares unchanged file data where the filesystem supports it. `join` prints the job's exit status, report, and any captured changes. Open the cited test definition and install any required Linux dependencies. Then try the test command at the marsh prompt. Ctrl-C stops the foreground job and its children. This task normally returns a report without a patch. For a task that changes code, continue with [Make independent changes in parallel](https://runmar.sh/docs/guides/race-two-agents.md). ## Inspect and clean up Still at the **marsh prompt**, inspect what ran: ```sh marsh jobs --tree marsh results ``` The tree shows parent-child relationships. Results record exit status, timing, and whether marsh confirmed container deletion. `--keep` preserved this split's output. Once you have read it, replace `YOUR_SPLIT_ID` below with the ID from the first join output line, remove the saved copy, and leave the shell: ```sh marsh splits rm YOUR_SPLIT_ID exit ``` Back in your **Mac terminal**, stop the VMs: ```sh marsh stop ``` Stop refuses while any marsh shells or jobs remain active. It preserves your project, guest home, and job records. ## Run an agent directly A direct command such as `claude -p 'Explain this repository'` mounts your original project read-write. The agent can change or delete files; packaged agents run without their own permission prompts or internal sandboxes. Your own shell commands, including tests, run unconfined in the shell VM. A test started inside a split branch begins in its fork but can write elsewhere in that VM. See the [security model](https://runmar.sh/docs/security.md). ## Agent credentials The example uses a proxy-managed Anthropic credential. For Codex, configure `sbx secret set openai` on your Mac before calling `codex exec`. `sbx secret ls` lists stored credentials. OAuth options are in [Signing in](https://runmar.sh/docs/kits.md#signing-in). Docker Sandboxes adds proxy-managed keys to outgoing requests; containers receive placeholders. Agent logins saved in the guest home are instead readable by other jobs sharing that home. ## Make it your shell Your dotfiles and agent settings live in `~/.marsh/home` on your Mac and appear under `~` inside marsh. They survive VM removal. See [Set up your marsh home](https://runmar.sh/docs/guides/set-up-your-home.md) for skills, startup files, and MCP connections. Vim and terminal Emacs are included. ## Agents running agents An agent can call registered commands from its shell tool. Each call starts a child job with its own container and Kit credentials. Try [Let an agent farm out work](https://runmar.sh/docs/guides/farm-out-work.md) for one bounded subtask. ## Run commands side by side Use `fanout { ... } | collect` for commands that can safely share files. It creates no project copies, so concurrent writers can interfere with each other. See [Fanout and collect](https://runmar.sh/docs/fanout.md). Continue with [Split and join](https://runmar.sh/docs/split.md) or [The shell](https://runmar.sh/docs/shell.md). --- # Guides After the [Quickstart](https://runmar.sh/docs/quickstart.md), use these recipes for work in your project: - [Make independent changes in parallel](https://runmar.sh/docs/guides/race-two-agents.md): separate CLI and API changes, with tests in each branch and on the combined tree. - [Review a change with a panel](https://runmar.sh/docs/guides/review-with-a-panel.md): two review questions about one diff. - [Let an agent farm out work](https://runmar.sh/docs/guides/farm-out-work.md): one child job and evidence you can check in the source. - [Review split results in a pipeline](https://runmar.sh/docs/guides/add-a-judge.md): optional comments from a downstream agent before you apply patches. - [Set up your marsh home](https://runmar.sh/docs/guides/set-up-your-home.md): personal skills, dotfiles, shared and per-Kit MCP connections and mounted directories. - [Build your own agent](https://runmar.sh/docs/guides/build-a-personal-agent.md): package any agent CLI (OpenCode, Claude Code, a claw) with your defaults, skills and tools as a marsh command. `split { ... } | join` creates project copies. `fanout { ... } | collect` runs on shared files. In another shell, or inside an agent job, use the CLI forms: `marsh split ::: LABEL CMD ARG... | marsh join` and `marsh fanout ::: LABEL CMD ARG... | marsh collect`. For the full behavior and options, see [Split and join](https://runmar.sh/docs/split.md), [Fanout and collect](https://runmar.sh/docs/fanout.md), and [Agents running agents](https://runmar.sh/docs/agents.md). Preview: [Docker Cloud](https://runmar.sh/docs/cloud.md) runs a `cloud { ... }` block in a cloud sandbox and writes its file changes back. Requires a Docker Cloud subscription with sandbox access. --- # Make independent changes in parallel Give one agent the CLI's JSON output and another the API's pagination tests. Each works on a project copy and runs the tests there. Inspect both patches, then test the combined change in your working tree. Use this recipe in an existing project with a CLI and a paginated API. Complete the [Quickstart](https://runmar.sh/docs/quickstart.md) to configure Anthropic. Both jobs use Claude. Adjust the prompts to name your CLI command, API handler, and test files. Choose responsibilities that can change independently; settle any shared response format before delegating. ## Make sure the tests run in marsh The examples below use `make test` as a placeholder; replace every occurrence with your repository's test command. Run it at the marsh prompt first and install any Linux arm64 dependencies it requires. Split copies omit Git-ignored files, so tests that depend on ignored artifacts need to recreate them in each copy. ## Run the tasks At the **marsh prompt**, adapt the scopes and test command, then run: ```sh split { cli: claude -p 'Add a JSON output option to the existing list command. Keep the current text output as the default. Change only CLI code and its tests; do not change the API or start other agents.' && make test api: claude -p 'Add tests for the existing pagination contract: the final page and an invalid cursor. Fix the API handler if those cases fail. Change only API code and its tests; do not change the CLI or start other agents.' && make test } | join --keep ``` Both branches start from the same snapshot. `&&` runs a branch's tests only after its agent exits successfully. The agent container mounts only its fork of the project; the test command runs as you in the shell VM, starting in that fork but unconfined. ## Read the result `join` prints each branch's status, output, and patch. A zero status here means the agent and that branch's test command both exited successfully. A failed branch makes `join` exit nonzero without discarding its sibling's result. `--keep` saves both copies and their patches. Read the changed-file lists and diffs. Check that the CLI preserves existing text output, and that the API tests exercise the intended cursor behavior. The prompts do not enforce file boundaries. If both agents changed shared code, resolve the overlap before applying their patches together. ## Apply one Use the split ID from the first join output line. If you want both patches, apply them one at a time, stopping if either check fails. Replace `YOUR_SPLIT_ID` and the test command below: ```sh split_id=YOUR_SPLIT_ID git apply --check ".marsh/split/$split_id/out/cli/diff.patch" && git apply ".marsh/split/$split_id/out/cli/diff.patch" && git apply --check ".marsh/split/$split_id/out/api/diff.patch" && git apply ".marsh/split/$split_id/out/api/diff.patch" && make test && git diff --check && git diff ``` The combined test run matters: each branch passed against its own starting copy, without the other branch's changes. Read the final diff before committing. You can also take only one patch and run the tests on that tree. If your files moved while the agents worked, see [Merging when your tree has moved](https://runmar.sh/docs/split.md#merging-when-your-tree-has-moved). Remove the saved split when you have finished inspecting its results: ```sh marsh splits rm "$split_id" ``` ## When it goes wrong Read a failed branch's stderr before rerunning it. A missing dependency can make a valid edit fail its check. A capacity refusal means the agent did not start. Ctrl-C stops the foreground split and its children. ## Cost This starts two billed agent runs and waits for both. Kept copies use disk space; copy-on-write cloning shares unchanged data where the filesystem supports it. See [Split and join](https://runmar.sh/docs/split.md) for copy restrictions and capture limits. ## Next [Review split results in a pipeline](https://runmar.sh/docs/guides/add-a-judge.md) creates a fresh split and sends its report to another agent before you apply anything. [Review a change with a panel](https://runmar.sh/docs/guides/review-with-a-panel.md) examines one existing diff from different angles. --- # Review a change with a panel Ask two reviewers different questions about the same uncommitted change. Each gets a project copy, so an unexpected agent edit stays out of your working tree. Use a Git repository with a change visible in `git diff HEAD`, and finish the [Quickstart](https://runmar.sh/docs/quickstart.md) first. Both reviewers below use Claude and the same Anthropic credential. The review covers staged and unstaged changes to tracked files; it does not include untracked files. ## Run the panel At the **marsh prompt**: ```sh split { security: claude -p 'Review git diff HEAD for security defects. Do not edit files or start other agents. For each finding, give the file, line, and a concrete failure scenario. Say when you found none.' coverage: claude -p 'Review git diff HEAD for changed behavior that tests do not cover. Do not edit files or start other agents. Point to the behavior and the missing test; distinguish guesses from findings.' } | join --keep ``` Both reviewers start from the same project snapshot, including uncommitted work. The prompts name the same diff and assign different concerns. If you want a committed branch reviewed instead, change **both** prompts to the same base comparison, such as `git diff main...HEAD` when your base is `main`. Check each finding against the source and existing tests before making a fix. If a reviewer edited files, `join` also shows that diff. Inspect it separately from the findings and leave it unapplied unless you want the change. ## Run checks without copies Run the repository's real checks alongside the review or afterward. The following recipe assumes your project already has working `make lint` and `make typecheck` targets; substitute its documented commands if not. Verify they work on Linux arm64 before using them here. ```sh fanout { lint: make lint types: make typecheck } | collect ``` `fanout` uses your real files. Only use this form when the commands can safely share a directory: checks that rewrite the same cache, fixture, or generated file may interfere with each other. `collect` prints results in declaration order and reports a nonzero status if a branch fails. These checks run as you in the shell VM, without confinement to the project. ## Finish the review Make or request the intended fix, run the relevant checks, and review the resulting diff. Remove the saved panel with the split ID printed by `join`: ```sh marsh splits rm YOUR_SPLIT_ID ``` ## Limits and failure modes This panel starts two billed agent runs. If a reviewer fails, `join` reports the failure and keeps the copies. Ctrl-C stops the foreground panel and its child jobs. For copy restrictions and capture limits, see [Split and join](https://runmar.sh/docs/split.md). ## Next [Make independent changes in parallel](https://runmar.sh/docs/guides/race-two-agents.md) uses split for edits. [Let an agent farm out work](https://runmar.sh/docs/guides/farm-out-work.md) starts a review from inside another job. See [Split and join](https://runmar.sh/docs/split.md) for capture limits and full patch paths. --- # Let an agent farm out work Ask an agent to delegate one code question, then verify the child's answer. The child runs from the parent's shell tool and appears beneath it in the job tree. Use an existing repository with a CLI and complete the [Quickstart](https://runmar.sh/docs/quickstart.md) first. This example uses Claude for both jobs with the same Anthropic credential. ## Start with one subtask At the **marsh prompt**: ```sh prompt='Use your shell tool to run this one child command: marsh run --no-spawn claude -p \ "Find the code that writes CLI output and a test that exercises it. Cite files and lines. If no test exists, say where you looked. Do not edit files." # Review split results in a pipeline Pipe a split's report to another agent for comments on the proposed changes. You read those comments and decide what to apply. Use an existing project with a CLI, a paginated API, and a working Linux test command, as in [Make independent changes in parallel](https://runmar.sh/docs/guides/race-two-agents.md). Configure Anthropic before starting. Adapt the prompts to your code and replace `make test` with the repository's actual check. This example creates its own split; it does not need a saved result from another guide. ## Send the join report to a reviewer At the **marsh prompt**: ```sh review='Read both branch reports and patches. Look for conflicting assumptions between CLI JSON output and API pagination, and for missing tests. Give comments I can check in the code. Do not edit files, apply patches, or start other agents.' split { cli: claude -p 'Add JSON output to the existing list command, preserving text output by default. Change only CLI code and tests.' && make test api: claude -p 'Test the existing pagination contract for the final page and invalid cursors. Fix the API handler if needed. Change only API code and tests.' && make test } | join --keep | claude -p "$review" marsh splits ``` The last agent receives the join report on stdin, including branch statuses and printed diffs. It runs directly against your original project, with read-write access despite the request not to edit. The editing agents work on their private project copies. Use a separate split for the reviewer too if its possible writes must stay off your original files. `marsh splits` lists the kept run so you can find its ID without relying on the reviewer's reply. Inspect the saved patches and test output yourself. For a large change, `join` may truncate a printed diff; use the full `out/LABEL/diff.patch` file when checking a comment about omitted code. ## Apply or discard the changes Apply the patches you want using the [parallel guide's application steps](https://runmar.sh/docs/guides/race-two-agents.md#apply-one), then run your checks on the combined tree. Substitute this run's ID, not one from a previous session. When finished, remove this run's saved copies: ```sh marsh splits rm YOUR_SPLIT_ID ``` ## Cost and limits This starts three billed agent runs. The reviewer's input grows with the patches and can exceed its context limit. A failed reviewer leaves the kept split available; the pipeline does not apply patches or remove the copies. For reviewers examining one existing diff, use [Review a change with a panel](https://runmar.sh/docs/guides/review-with-a-panel.md). --- # Set up your marsh home From your Mac, put marsh's dotfiles and agent settings in `~/.marsh/home`. Inside marsh, that directory appears as your home (`~`). Its files survive `marsh stop` and VM resets. ## Agent settings and skills Use these paths on your Mac: | Agent | Skills | Global instructions | |---|---|---| | Pi | `~/.marsh/home/.agents/skills/NAME/SKILL.md` | `~/.marsh/home/.pi/agent/AGENTS.md` | | Codex | `~/.marsh/home/.agents/skills/NAME/SKILL.md` | `~/.marsh/home/.codex/AGENTS.md` | | Claude | `~/.marsh/home/.claude/skills/NAME/SKILL.md` | `~/.marsh/home/.claude/CLAUDE.md` | Put shell startup files here too: `~/.marsh/home/.bashrc` on the Mac is `~/.bashrc` inside marsh. See [Choosing your shell](https://runmar.sh/docs/shells.md) for startup files and [The shell](https://runmar.sh/docs/shell.md#editor-settings) for editor settings. Jobs share this home, including any saved logins and history. If you keep dotfiles in Git, track the files you choose rather than the entire home. For example, add an alias and prompt in the **marsh shell**: ```sh cat >> ~/.bashrc <<'EOF' alias ll='ls -lah' PS1='\w \$ ' EOF source ~/.bashrc alias ll ``` ## Connect an MCP server Save connections for one registered command in `~/.marsh/sbxenvs/NAME.yaml`. Create the `sbxenvs` directory if needed. For example, save this as `~/.marsh/sbxenvs/pi.yaml` on your Mac: ```yaml schemaVersion: "1" mcp: servers: - name: granola url: https://mcp.granola.ai/mcp ``` Exit your marsh shells, run `marsh stop` from your Mac terminal, then start marsh again. The next Pi job loads the connection. If it reports that sign-in is required, run this on your Mac, then retry: ```sh sbx mcp auth granola ``` To load a connection for every Kit and the shell, put it in `~/.marsh/sbxenv.yaml` instead. Shared entries are added to each command's entries; do not repeat the same server in both files. Loaded servers are shared by jobs in that Kit VM, across projects. Removing an entry stops future automatic loading; it does not remove the server's Docker Sandboxes registration or sign-in. Manage those with `sbx mcp` on the Mac. ### Local servers Use `command` and `args` instead of `url`: ```yaml schemaVersion: "1" mcp: servers: - name: my-reader command: /absolute/path/to/my-reader args: ["--readonly"] ``` **Local MCP servers run on your Mac with your account's permissions.** Keep their executable, scripts, and configuration outside directories that jobs can modify. Use the server's host-side authentication setup; keep keys out of YAML and command arguments. ## Mount a reference directory Add `additionalWorkspaces` to the same YAML file. For example, this makes an existing Mac directory available to Pi at `/Users/me/reference`, read-only: ```yaml schemaVersion: "1" additionalWorkspaces: - path: /Users/me/reference readOnly: true ``` Replace the path with your directory's absolute path. YAML paths do not expand `~` or environment variables. Set `readOnly: true` for files the agent should only read; the default is read-write. You can include both `mcp` and `additionalWorkspaces` in one file. Restart marsh after saving, as above. Child jobs inherit their parent's extra directories. Agents in a split do not receive extra directories. With a different `MARSH_HOME`, use its `home`, `sbxenv.yaml`, and `sbxenvs` paths instead. Ephemeral-home sessions skip these additions. See [Configuration](https://runmar.sh/docs/configuration.md#servers-and-extra-directories) for file rules and limits, [Troubleshooting](https://runmar.sh/docs/troubleshooting.md#environment-files-and-mcp-connections) for setup errors, and [Security](https://runmar.sh/docs/security.md#mcp-connections) for MCP access. --- # Build your own agent marsh runs any agent CLI as a command in its own sandbox: Claude Code, Codex, OpenCode, a claw, or something you wrote. Your own agent is one of those plus your choices: default model, skills, instructions and tools. You package it once as a **Kit**, and then `myagent` works in every marsh shell, can start other agents as child jobs, and never sees an API key. This guide builds `myagent` from [OpenCode](https://opencode.ai). Every step says what to change for a different agent. You need marsh 0.1.8 or later, Docker Desktop with Buildx, and a model key stored in SBX (`sbx secret set anthropic`). ## What you'll make A folder outside any project you open in marsh: ```text myagent/ ├── myagent.yaml # the Kit: network, credentials, skills folder ├── Dockerfile # the agent on marsh's base image ├── entrypoint.sh # starts the agent with your settings ├── config/ │ └── opencode.json # your defaults └── skills/ └── release-notes/ └── SKILL.md # a skill of your own ``` ## 1. The image ```dockerfile FROM dhi.io/debian-base:trixie-dev AS download ARG OPENCODE_VERSION=1.19.3 ADD https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-arm64.tar.gz /tmp/opencode.tar.gz RUN tar -xzf /tmp/opencode.tar.gz -C /tmp opencode FROM docker.io/mcavage/marsh-shell:0.1.8 COPY --from=download /tmp/opencode /usr/local/bin/opencode COPY config /opt/myagent/config COPY skills /opt/myagent/config/skills COPY --chmod=0755 entrypoint.sh /usr/local/bin/myagent ENTRYPOINT ["/usr/local/bin/myagent"] ``` Start from `marsh-shell`, at the version of your marsh. The same image boots the sandbox your jobs run in, which needs the Docker Engine and plumbing that `marsh-shell` provides. It's built on a [Docker Hardened Image](https://docs.docker.com/dhi/) and has no package manager, so fetch or build your agent in a separate stage and copy the result in. Use a hardened image there too: the `-dev` variants, like `dhi.io/debian-base:trixie-dev`, include the usual build tools and are free with the Docker sign-in you already use for `sbx`. **Another agent:** change the download stage. Anything that ends up as a binary, or a runtime plus your agent's files, works. ## 2. The entrypoint ```sh #!/bin/sh set -eu # marsh names the home to use; settings and sessions persist there. export HOME="${MARSH_SELECTED_HOME:-$HOME}" # Your defaults and skills, shipped in the image. export OPENCODE_CONFIG_DIR=/opt/myagent/config # SBX replaces this placeholder with your real key at Anthropic's API. [ "${SBX_CRED_ANTHROPIC_MODE:-none}" = none ] || export ANTHROPIC_API_KEY=proxy-managed exec /usr/local/bin/opencode "$@" ``` Three things every agent's entrypoint does: - **Use `MARSH_SELECTED_HOME` as `HOME`**, so the agent's sign-ins, settings and history live in your marsh home and persist between runs. - **Point the agent at your defaults**, without overwriting settings you later change by hand. Here that's OpenCode's `OPENCODE_CONFIG_DIR`. Other agents have a flag, an environment variable, or a config file to seed on first run. - **Set a placeholder key** when SBX reports it manages one. The agent sends the placeholder, and SBX's proxy swaps in the real key only on the way to the provider's API. ## 3. Your defaults, skills and instructions `config/opencode.json`: ```json { "$schema": "https://opencode.ai/config.json", "model": "anthropic/claude-sonnet-5-5", "autoupdate": false, "instructions": ["/run/marsh/context.md"], "skills": { "paths": ["/opt/myagent/kit-skills"] } } ``` - **Skills** in `skills/` ship in the image. OpenCode finds them inside its config folder. - **`/run/marsh/context.md`** is written by marsh for every job. It tells your agent which commands it can start, and how (`marsh fanout`, `marsh split`, `marsh jobs --tree`), and it includes any instructions the Kit declares. Pass it to your agent as instructions or as an extra system prompt. - **`/opt/myagent/kit-skills`** is where skills added later by a mixin appear (step 7). Load it as a second skills folder. **Another agent:** find how it takes an extra instructions file and extra skill folders. For example, marsh's Pi Kit uses `--append-system-prompt FILE` and `--skill DIR`. ## 4. The Kit file `myagent.yaml`: ```yaml # syntax=docker/sandbox-kit:3 schemaVersion: "3" kind: workload displayName: My agent description: OpenCode with my defaults and skills licenses: [MIT] dockerfile: ./Dockerfile capabilities: - type: com.docker.sandbox/sbx@1 - type: com.docker.sandbox/network-policy@1 config: runtime: allow: - api.anthropic.com:443 - models.dev:443 - type: com.docker.sandbox/credential@1 description: Anthropic API key config: service: anthropic phase: runtime apiKey: name: ANTHROPIC_API_KEY proxyManaged: true inject: - {domain: api.anthropic.com, header: x-api-key, format: "%s"} - type: com.docker.sandbox/agent-skills@1 config: path: /opt/myagent/kit-skills ``` - **Network:** the hosts your agent may reach. OpenCode also reads its model list from `models.dev`. - **Credential:** which stored key SBX sends, and to which host and header. Add one entry per provider your agent uses. - **Skills folder:** where mixins may add skills (step 7). ## 5. Try it Register the folder as a command. `marsh status --json` prints your control directory (`control_home`). Add this to its `commands.json`, keeping any existing entries: ```json { "myagent": "/Users/you/kits/myagent" } ``` Then restart marsh's daemon so it reads the change, and run your agent: ```sh marsh stop marsh myagent run "Summarize this repository in three bullets" ``` The first run builds the image and boots its sandbox, which takes a few minutes. After that it starts in seconds. Edit any file and run again; marsh rebuilds when the folder changes. Inside marsh, other agents can call yours, and yours can call them: ```sh fanout { mine: myagent run "review the diff" claude: claude -p "review the diff" } | collect ``` ## 6. Publish and install Push the Kit to a registry: ```sh docker buildx build --platform linux/arm64 --push -f myagent.yaml \ -t registry.example.com/you/myagent:1.0 -t registry.example.com/you/myagent:stable . ``` Remove the local `myagent` entry from `commands.json`, then install it the way anyone else would: ```sh marsh kit install myagent registry.example.com/you/myagent:stable ``` marsh then moves `myagent` to whatever `stable` names, each time the daemon starts and once an hour. ## 7. Let others add their own skills A team can add skills and instructions to your agent without changing your image: they publish a **mixin** and combine it with your Kit in a **kit set**. marsh links the mixin's skills into `/opt/myagent/kit-skills` and adds its instructions to `/run/marsh/context.md`. See [Adding skills and instructions with mixins](https://runmar.sh/docs/kits.md#adding-skills-and-instructions-with-mixins). ## 8. Give it tools MCP servers only your agent should load go in `~/.marsh/sbxenvs/myagent.yaml` on your Mac; the shared ones go in `~/.marsh/sbxenv.yaml`. SBX's MCP gateway runs them and holds their credentials. See [Set up your marsh home](https://runmar.sh/docs/guides/set-up-your-home.md). ## Examples - marsh's own [`claude`, `codex` and `pi` Kits](https://github.com/mcavage/marsh/tree/main/kits): small entrypoints, each passing `context.md` to its agent its own way. - [Pix](https://github.com/mcavage/pix/tree/main/marsh): Pi with a skills library, themes and a status display. A larger example of the same steps. ## Before you share it - Run it next to the packaged agents in a fresh marsh home. Only `myagent` should see your skills and defaults. - Look at what the image contains, not just your Git diff, for private skills, account names, endpoints or keys. - Make one real model call and one child job. A version or tool listing proves little. --- # 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](https://runmar.sh/docs/install.md), run this from your project directory: ```sh 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: ```sh 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. `pwd` prints 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 : `$HOME` has the same path as your Mac home. It is backed by `~/.marsh/home` (the guest home) and persists between shells, so put dotfiles there. Set `MARSH_HOME` to use another scope (see [Configuration](https://runmar.sh/docs/configuration.md)). User : Your Mac user name and UID. The shell user has passwordless `sudo` and a private Docker Engine (`docker run` works) inside the VM. Packages installed with `sudo apt-get` last as long as the VM. Kit jobs start as nonroot users in separate containers with no Docker or SBX sockets. See the [security model](https://runmar.sh/docs/security.md) 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: ```sh 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](https://runmar.sh/docs/split.md) | | `fanout { a: ...; b: ... } \| collect` | Branches on the same files; see [Fanout](https://runmar.sh/docs/fanout.md) | | `ps --marsh`, `top --marsh` | This project's shell, jobs, and agent sessions | | `acp run`, `acp ask`, ... | Agent sessions over ACP; see [ACP](https://runmar.sh/docs/acp.md) | | `mcp publish`, `mcp load`, ... | Publish a pipeline as an MCP tool; see [MCP](https://runmar.sh/docs/mcp.md) | | `marsh jobs`, `marsh split`, ... | The `marsh` CLI works inside the shell too | | `xdg-open URL`, `open URL` | Open a web link in your Mac's browser; see [Opening links](https://runmar.sh/docs/agents.md#opening-links) | `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](https://runmar.sh/docs/kits.md). ## What the shell is The shell is [Brush](https://github.com/reubeno/brush), a Bash-compatible shell written in Rust. marsh never runs Bash to implement itself. The [upstream record](https://github.com/mcavage/marsh/blob/main/docs/upstream/brush/README.md) 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](https://runmar.sh/docs/shells.md). ## 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](https://runmar.sh/docs/guides/set-up-your-home.md) 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 `RETURN` trap does not fire. - `select` and `${x~~}` are not supported. - `(( m["a b"]=1 ))` and `m[a b]=1` do not parse. - A trapped signal runs its trap after the current pipeline, not in the middle of a builtin. A blocking `read` from a terminal is not interrupted. - `set -u` on an unset variable exits with status 1. - Error messages use Brush's wording and add a `(col N)` suffix. - `printf '%(fmt)T'` uses the process `TZ`, 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](https://runmar.sh/docs/design/bash-compatibility.md). ### Report a difference [Open an issue](https://github.com/mcavage/marsh/issues) with the smallest script that shows the difference and Bash's output. Fixes go into the Brush patch queue. Next: [Split and join](https://runmar.sh/docs/split.md), [Choosing your shell](https://runmar.sh/docs/shells.md). --- # Choosing your shell Brush is the default shell. To use GNU Bash or zsh with startup files from your [guest home](https://runmar.sh/docs/shell.md#where-you-are), select the shell VM's copy: ```sh marsh --shell zsh -c 'echo $MARSH_SHELL' ``` Registered commands (`claude`, `codex`, ...), `acp`, `mcp`, and the `marsh` CLI keep working. The `split { }` and `fanout { }` forms are [Brush-only](#what-is-brush-only). ## Selecting | Where | Form | Scope | |---|---|---| | flag | `marsh --shell NAME [ARGS...]` | this launch | | environment | `MARSH_SHELL=NAME marsh` | this launch | | home default | `marsh config shell NAME` | every launch with this `MARSH_HOME` | `NAME` is `marsh` (Brush), `bash`, or `zsh`. Precedence, highest first: `--shell`, then `MARSH_SHELL`, then the home default. With none set, you get Brush (`marsh`). `marsh config shell` with no name prints the default. The default is stored in `$MARSH_HOME/config.json`, outside the guest home. An unknown name exits 2 before any VM starts and prints `unknown shell "NAME" (choose marsh, bash, or zsh)`, preceded by which of the three sources supplied the name. To run a script named `config`, use `marsh ./config`. Arguments after the marsh options go to the chosen shell unchanged: `marsh --shell bash script.sh` runs a script; `marsh --shell bash -l` opens a login shell. ## What keeps working - Registered names (`claude`, `codex`, ...), from the shell or from any program it runs. Each starts a Kit job. - `acp`, `mcp`, `ps`, and `top` remain available. Without `--marsh`, `ps` and `top` run the system binaries. - The `marsh` CLI keeps working. A Kit you install later with `marsh kit install` is available at once. - Terminal input and window resizing work as before. - Ctrl-C reaches a local command or a Kit job. - marsh returns the chosen shell's exit status, or 128+N if a signal kills it. - Your guest home persists between sessions. - Session cleanup still covers everything the shell starts. Registered names, `acp`, `mcp`, `ps`, and `top` are links in a per-session directory at the front of `PATH`. `$MARSH_SESSION_BIN` names it. ## What is Brush-only - `split { ... } | join` and `fanout { ... } | collect`. Use [`marsh split`](https://runmar.sh/docs/split.md#from-plain-bash-cli-form) and [`marsh fanout`](https://runmar.sh/docs/fanout.md#from-any-shell) instead. - `-b LABEL=STRING` strings always run in a fresh Brush shell. Your bash or zsh aliases and functions are not visible there. - A file without a shebang runs by your chosen shell's rule, not with Brush. ## Startup files Put your `.bashrc`, `.zshrc`, and other startup files in the guest home, backed by `$MARSH_HOME/home` and mounted as `$HOME` in the shell. marsh does not copy your Mac's dotfiles or modify these files. marsh starts the shell with a generated layer in a private per-session directory. The layer sources your files. ### bash marsh runs `bash --rcfile LAYER`. For an interactive non-login shell, the layer sources `~/.bashrc`, then moves the session command directory back to the front of `PATH`. Noninteractive Bash inherits the session `PATH`. marsh removes `BASH_ENV` and `ENV` before starting the selected shell. ### zsh marsh sets `ZDOTDIR=LAYER`. - `.zshenv` in the layer sources yours and pins `PATH`. It reads yours from `$ZDOTDIR` if your environment sets it, else from `$HOME`. - Interactive login shells read the layer's `.zprofile`, and interactive shells read its `.zshrc`. Each sources your corresponding file. - Interactive shells return `ZDOTDIR` to your directory in `.zshrc`; noninteractive shells do so in `.zshenv`. Later startup files, including `.zlogin` and `.zlogout` for login shells, are read from your directory directly. ### Login shells A login bash (`-l`) reads your profile instead of the layer. If the profile rewrites `PATH`, the session directory may no longer be first. To keep it first, prepend `$MARSH_SESSION_BIN` to `PATH` at the end of your profile. ## Variables | Variable | Value | |---|---| | `MARSH_SHELL` | the running shell | | `MARSH_SESSION_BIN` | the session command directory | Next: [The shell](https://runmar.sh/docs/shell.md), [Commands and Kits](https://runmar.sh/docs/kits.md). --- # Commands and Kits A Kit packages a command's image, network policy, and credentials for Docker Sandboxes. marsh registers it under a shell name and runs each invocation in a fresh nonroot container. For example, in your [marsh shell](https://runmar.sh/docs/shell.md): ```sh shell -c 'uname -sm' ``` `shell` is a packaged command. To add your own, use `marsh kit install NAME REPOSITORY:TAG`, substituting its published image. marsh uses the native Docker Sandboxes Kit v3 format. Commands update themselves; see [Updates](#updates). ## The default commands | Command | Kit | What it is | |---|---|---| | `claude` | `marsh-claude` | Claude Code, and its ACP adapter | | `codex` | `marsh-codex` | OpenAI Codex CLI, and its ACP adapter | | `pi` | `marsh-pi` | The Pi coding agent, and its ACP adapter | | `shell` | `marsh-shell` | A plain `/bin/sh`, with no credentials | | `pix` | `marsh-pix` | Upstream Pi with public skills, orchestrated by marsh | | `hermes` | `marsh-hermes` | Hermes Agent, sharing the marsh home's skills folder | Releases pin the first four images by digest (`docker.io/mcavage/marsh-*@sha256:...`); the list is in the installed `libexec/marsh/commands.json`. `pix` and `hermes` follow their `:stable` tag: a release build adds each one shortly after the daemon first starts, which needs the network. Until then the name is not there yet; a problem shows in `marsh status`. If you already have your own command with one of these names, marsh leaves it alone. `marsh kit uninstall pix` removes it for good, until you run `marsh kit install pix` again. A foreground call that boots a Kit VM prints `[starting NAME worker VM…]`. To prepare VMs before the prompt, start marsh with `marsh --load KIT,...` or `marsh --load all`. ### Signing in marsh has no login of its own. Docker Sandboxes (`sbx`) holds the credentials on your Mac. Run `sbx login` once, then give `sbx` a credential for each provider: | Host command | Used by | |---|---| | `sbx secret set anthropic` | `claude` and `pi` | | `sbx secret set openai` | `codex` | `sbx secret import` takes keys from your environment variables instead. The Claude and Codex Kits also accept an OAuth login that `sbx` holds. The `sbx` proxy adds the credential to the agent's requests, and the container sees a placeholder. If nothing is bound, an agent can still sign in from inside its container (`codex login`, or Claude's `/login`). That login is stored in the guest home (`~/.marsh/home` on the Mac) as ordinary files, and every job in the scope can read it. Prefer `sbx secret`. The Pi Kit declares an Anthropic API key only. `shell` has no credentials. ### Updates Each Kit image pins its agent's version. Each time the marsh daemon starts, and once an hour while it runs, marsh checks in the background whether a command's tag names a newer image, so you get a new Claude Code, Codex, Pi, Pix or Hermes without waiting for a marsh release. It never delays what you are doing, and an offline check is reported only in `marsh status`. - The packaged commands follow the image tag of your release channel (`:latest` for stable, `:nightly` for nightly) on a release build. A Kit you installed from a tag follows that tag. - The new image is prepared first. A command whose Kit is running jobs waits and switches when they finish. A failure keeps the old image. An image that needs a newer marsh (its `org.marsh.kit.contract` label) is refused. - After a switch, the next time you run that command at a terminal, or open a marsh shell, marsh prints one line on stderr, once: ```text marsh: claude updated (2.1.305 → 2.1.307) ``` A rebuild with the same version switches without a notice. - `marsh status` shows when the last check ran, any problem, commands waiting for their jobs, and recent updates (`kit_updates` in `marsh status --json`). - To update right now, run `marsh stop` when nothing is running, then use marsh again. - To turn updates off for a `MARSH_HOME`, run `marsh config auto-update off`, or start the daemon with `MARSH_AUTO_UPDATE=0`. That also stops `pix` and `hermes` from being added. A command you installed by digest never changes, and neither does a packaged command you pointed at a different image or a Kit folder in `commands.json`. A packaged command still updates from any image of its own repository. ACP agents for a packaged command, or for a command installed from a tag, follow its updates. Upgrading marsh itself also brings the versions it ships; see [Upgrade](https://runmar.sh/docs/install.md#upgrade). Inside marsh, `claude --version`, `codex --version`, and `pi --version` show the installed versions. Resetting a VM alone does not update its pinned image. The packaged Claude Kit disables self-updates in CLI and ACP sessions; `claude update` and `claude upgrade` exit 125. ## How a call runs When you invoke a registered command: 1. The shell asks the marsh daemon on your Mac to start a job. 2. The daemon finds a warm VM for the command's Kit, or creates one. 3. Inside that VM, a fresh container starts from the Kit image. It runs as a nonroot user with your UID. 4. Your project is mounted read/write at its Mac path. In a split, the job instead mounts its fork at the fork's own host path. Your guest home is mounted read/write as `$HOME` in both cases. 5. stdin, stdout, stderr, signals, and the exit status pass through as for a local program. A child job started by another job gets no terminal ([Agents running agents](https://runmar.sh/docs/agents.md#limits-and-messages)). 6. When the program exits, the container is deleted and the deletion is checked. The VM stays warm for the next call. Each call gets its own container, including concurrent and background calls. Up to 16 jobs run at once per daemon. An agent can edit or delete the project files its container mounts. The packaged agent Kits disable their agents' own sandboxes and permission prompts ([details](https://runmar.sh/docs/agents.md#agent-sandboxes-and-permission-prompts)). Use [`split`](https://runmar.sh/docs/split.md) to give an agent a private project copy. The guest home remains shared even in a split. ### Network and credentials Credentials go through the Docker Sandboxes proxy; see [Signing in](#signing-in). Each Kit VM has the network policy its Kit declares. The `codex` Kit, for example, allows the listed OpenAI endpoints, GitHub download hosts, npm, and package repositories. The exact hosts and ports are in the Kit's descriptor (`kits/marsh-codex/codex.yaml`). ### Limits per job | Limit | Default | |---|---| | CPUs | 2 | | Memory | 4 GiB | | Processes | 4,096 | | Writable layer | 10 GiB | | Output | 256 MiB | | Wall time | 24 hours | To change these, see [Configuration](https://runmar.sh/docs/configuration.md#job-limits). ## Agents inside jobs Inside a job, every registered name is on `PATH`. An agent can start another agent as a child job. See [Agents running agents](https://runmar.sh/docs/agents.md). ## Adding a command ```sh marsh kit install mytool registry.example.com/me/mytool-kit:stable marsh kit uninstall mytool ``` `marsh kit install NAME REPOSITORY:TAG` asks `sbx` which image the tag names now, prepares that image, and registers NAME. marsh records the digest, never the tag, so the command runs exactly that image until an [update](#updates) moves it to a newer one. You can also install a digest (`REPOSITORY@sha256:DIGEST`); such a command never changes. The shell you run it from sees the name immediately; other open shells see it once reopened. An existing name, including a packaged one, is refused. `marsh kit install pix` or `marsh kit install hermes`, with no reference, installs that Kit right away. ### Removing a command `marsh kit uninstall NAME` removes a command you installed, including `pix` or `hermes`. For a packaged command that was updated, it returns to the version your marsh shipped. The tags installed commands follow are recorded in `kit-sources.json` next to `commands.json`. Updates keep their record in `kit-updates.json` in the same directory. ### Changing or replacing a command Edit `commands.json` in the scope's host control directory. `marsh status` prints that directory. Entries there override the packaged ones by name: ```json { "mytool": "registry.example.com/me/mytool-kit@sha256:0123…", "local-tool": "/Users/you/kits/local-tool" } ``` Each value is one of: - an image reference pinned by digest (tags are refused here; `marsh kit install` resolves a tag to its digest and updates follow it); - the absolute path of a Kit source directory. Manual edits take effect when the daemon starts: finish active work, exit open shells, run `marsh stop`, then launch marsh again. A worker reset alone does not reload `commands.json`. A malformed `commands.json` stops the daemon from starting, and the error names the file. ### Rules for names and sources - Names are 1 to 128 letters, digits, `_`, or `-`, and do not start with `-`. - These names are reserved: `marsh`, `fanout`, `collect`, `acp`, `mcp`, `ps`, `top`. - A source directory must be outside your project and guest home, so the normal project and home mounts do not expose it to guest code. - Running a Kit from source needs Docker Desktop with Buildx on the Mac. - Docker Sandboxes must allow the image's registry in its Kit sources policy. ## Writing a Kit A Kit is a directory with a descriptor and a Dockerfile. The `shell` Kit is a small example: ```yaml # syntax=docker/sandbox-kit:3 schemaVersion: "3" displayName: Generic shell kind: workload dockerfile: ./shell.dockerfile capabilities: - type: com.docker.sandbox/sbx@1 ``` The image's `ENTRYPOINT` receives the command's arguments. It runs as a nonroot user in the caller's directory, with these mounts and variables: - `HOME` set to the mounted guest home. The same path is in `MARSH_SELECTED_HOME`. - `/run/marsh/bin` first on `PATH`. It holds the registered commands, an in-job `marsh`, and `xdg-open` and `open` ([opening links](https://runmar.sh/docs/agents.md#opening-links)). `BROWSER=xdg-open` unless the job already has a `BROWSER`. - `/run/marsh/context.md`, a short text telling an agent what it may start, followed by the Kit's own instructions (`agent-context`). The same image also boots the outer Kit VM. That VM must provide Docker Engine and the stock sandbox runtime plumbing for marsh's fresh job containers; a standalone minimal Python or Alpine image is not enough. Derive from a suitable packaged marsh workload/base rather than putting host Docker sockets into jobs. Keep tool state under `$HOME` so it persists between calls. Other jobs in the scope can read it. The packaged Kits are in [`kits/`](https://github.com/mcavage/marsh/tree/main/kits). They show how the agent Kits pass `context.md` to their agent. ### Adding skills and instructions with mixins To add to an existing agent without changing its image, publish a mixin with your skills and instructions, then combine it with the agent in a [kit set](https://docs.docker.com/ai/sandboxes/customize/author/kit-sets/): ```yaml # syntax=docker/sandbox-kit:3 schemaVersion: "3" kind: set displayName: Team Pix kits: - ref: docker.io/mcavage/marsh-pix digest: sha256:… - ref: registry.example.com/team/pix-skills digest: sha256:… ``` Publish the set with Docker Buildx and install it like any other Kit (`marsh kit install team-pix registry.example.com/team/pix:stable`). Each job receives what the set declares: - **Instructions** (`agent-context`) are added to `/run/marsh/context.md`. - **Skills** (`agent-skill@1`) are linked, read-only, into each skills folder the agent declares (`agent-skills@1`). A folder inside your home or project is skipped, so marsh never creates files there. marsh reads these from the image's published descriptor the first time a command runs, and keeps the result. A Kit run from a source directory has none. ### Publishing a Kit Build and push the Kit with Docker Buildx to get a digest reference. In the repository, `make kit-publish` does this for the packaged Kits ([Kit publication](https://runmar.sh/docs/design/kit-publication.md)). To share a Kit, post it in [Show and tell](https://github.com/mcavage/marsh/discussions). ## Removing VMs | Command | Removes | |---|---| | `marsh workers reset codex` | The idle Codex Kit VM | | `marsh workers reset all` | All idle Kit VMs | The next call creates a new VM. Use this to clear a quarantined VM ([Troubleshooting](https://runmar.sh/docs/troubleshooting.md#cleanup-uncertain-and-quarantine)). Next: [Troubleshooting](https://runmar.sh/docs/troubleshooting.md), [Configuration](https://runmar.sh/docs/configuration.md). --- # Agents running agents An agent running in marsh can call registered commands to start child jobs. Each child gets a fresh container, appears under its parent in `marsh jobs --tree`, and stops when its parent ends. Ctrl-C cancels the whole foreground job tree. With [marsh installed](https://runmar.sh/docs/install.md) and both [providers configured](https://runmar.sh/docs/kits.md#signing-in), try this from your marsh shell: ```sh claude -p "use your Bash tool to run: codex exec 'reply with the number 7'" marsh jobs --tree ``` If Claude makes the requested call, Codex appears as its child in the tree. The child inherits its parent's project mounts and shared guest home, with its own Kit's network policy and credentials. A [split](https://runmar.sh/docs/split.md) can narrow its project mount to a fork. Any registered command is allowed by default, including another invocation of the same agent, subject to the [limits](#limits-and-messages) and [spawn set](#narrowing-what-an-agent-may-start). ## Signing in Configure each child's provider through [Docker Sandboxes](https://runmar.sh/docs/kits.md#signing-in), as you would for a direct call. ## Split from inside an agent An agent can split work across private forks and read the results back as patches. From a job, each branch must be `::: LABEL CMD ARG...`, naming a registered command. The child mounts its fork at the fork's own host path; the guest home is still shared. ```sh marsh split ::: fix codex exec 'fix the typo in notes.txt' ::: review claude -p 'review notes.txt' | marsh join ``` This assumes `notes.txt` exists. Both branches begin with the same snapshot; the reviewer does not see the fixer's edits. `marsh join` prints one section per branch, with `kit:NAME` identifying the Kit and a patch for any captured changes. It never applies the patches. Use `marsh join --keep` if you want to inspect or apply them after join exits. See [Split and join](https://runmar.sh/docs/split.md). Shell branches (`marsh split -b`, `split { a: ...; b: ... }`) run only from your own shell. A job that asks for one is refused: ```text split: a: shell branches (-b, split { }) run only from the session shell ``` ## Parallel work on the same files Inside a job, `marsh fanout` runs commands concurrently in that job's container, sharing its files. A registered command starts a child job; other commands use the container's own tools. Writes can race. ```sh claude -p "run tests and lint at once with marsh fanout" ``` The agent can run argv branches directly: ```sh marsh fanout ::: tests make test ::: lint make lint | marsh collect ``` `collect` prints each branch's output in the order written, and exits with the first failing branch's status. This example needs the project's `test` and `lint` Makefile targets. From a job, branches must be `::: LABEL CMD ARG...`, as for `split`. The [fanout reference](https://runmar.sh/docs/fanout.md) also covers shell branches and Brush blocks for use from your own shell. ## Ctrl-C and cleanup Ctrl-C in your shell cancels the foreground job's whole tree: 1. Every job gets `INT`. 2. Anything still running 10 seconds later gets `KILL`. 3. marsh checks each container's deletion and records the cancellation and cleanup result. A job that had already ended keeps its completed state. 4. Your command exits 130. A job that ends cancels its own children the same way: `INT`, then `KILL` after 10 seconds. If marsh cannot confirm a job ended, `jobs --tree` shows it as `cleanup uncertain`. `marsh workers reset KIT` removes that Kit's VM and frees the slot ([Troubleshooting](https://runmar.sh/docs/troubleshooting.md#cleanup-uncertain-and-quarantine)). ## Narrowing what an agent may start To restrict child commands, set `MARSH_SPAWN` in your shell to a comma-separated list of names, or `none`. `marsh run --spawn a,b NAME` and `--no-spawn` do the same for one command. Nothing inside a job can widen the list. ```sh export MARSH_SPAWN=codex claude -p "use your Bash tool to run: shell -c true" ``` This limits which commands start. It does not limit what a permitted agent does to your project files. ## Limits and messages A refused call starts nothing and prints the reason on stderr. A registered-name link outside `MARSH_SPAWN` exits 126; other refusals, including those through `marsh run`, exit 125. `marsh status` counts refusals (`processes: N running, N refused, N held`). | Limit | Message | |---|---| | 16 jobs at once per daemon, all depths | `capacity: 16 jobs` | | Depth 4 (your command is depth 1) | `depth limit 4` | | The same Kit at most twice in a row | `claude → claude → claude refused: same-Kit chain limit 2` | | 8 live children per job | `fan-out limit 8` | | 128 launches per tree, counting refused child calls | `total limit 128` | | 4 distinct Kit VMs used by live jobs in one tree, by default | `tree already uses 4 Kit VMs (...) (MARSH_TREE_KIT_VMS)` | | At least one second must remain before the parent's deadline | `deadline: parent job ... has ... ms of wall time left` | | A name outside `MARSH_SPAWN` | `spawn refused: NAME not in this job's spawn set (MARSH_SPAWN)` | | No terminal for a child | `interactive child jobs are not supported yet` | A child job gets no terminal. If either stdin or stdout is a terminal, marsh refuses it. Run the command as `codex exec '...' # Split and join `split` runs parallel commands on private copies of your project, called *forks*. `join` shows their output and patches. It never applies a patch. From a marsh shell, ask two agents to work independently: ```sh split { a: claude -p 'append gamma to notes.txt' b: pi -p 'append gamma to notes.txt' } | join --keep ``` `--keep` leaves the results under `.marsh/split/ID/out/`. Review a branch's `diff.patch`, apply it with `git apply`, then remove the split with `marsh splits rm ID`. Keep results whenever you want to read or apply them after the pipeline ends. Registered agents mount their own fork of the project. A branch's shell commands, including tests, run with your shell VM privileges and can reach your real project ([branch types](#shell-branches-and--branches)). The shell examples below assume `notes.txt` contains `alpha` and `beta` on separate lines. Use a scratch project to try them. Agent examples also need [provider credentials](https://runmar.sh/docs/kits.md#signing-in). ## What split touches - marsh creates `.marsh/split/ID/` for the snapshot, forks, and results. In a Git project, it also creates `.marsh/.gitignore` if absent. It leaves the original repository's index, objects, and refs untouched. - Branches run in the shell VM or in a Kit's VM, not on the Mac. - In Git repositories, ignored files are not copied into forks. A split's root cannot be a linked Git worktree or a submodule checkout with a `.git` file; use the main checkout. - `marsh split` works from the marsh shell and from plain bash on the Mac. `split { }` works only in the marsh shell. Inside a job, only `:::` branches work. - `join` limits previews to 128 KiB of stdout per branch, 128 KiB of diff across branches, and the last 4 KiB of a failed branch's stderr. The whole rendering is capped at 192 KiB. Files in `out/` hold the full captured bytes. The snapshot comes from your current files, including uncommitted edits and nonignored untracked files. It is not a checkout of `HEAD`. marsh uses copy-on-write filesystem clones, falling back to ordinary copies on filesystems without cloning support. Other copy errors fail the split. Forks live and mount at their own host paths, such as `PROJECT/.marsh/split/ID/a`. In a shell branch, an absolute path to the original project still reaches your live files. A registered Kit command mounts the fork, not the original project root. ## Applying a result `git apply` needs no index. It works from the marsh shell and from a Mac terminal: ```sh split_id=$(marsh split -n -b fix='printf "alpha\nbeta\ngamma\n" > notes.txt') marsh join -- sh -c 'cat >/dev/null; git apply "$SPLIT_DIR/fix/diff.patch"' <<<"$split_id" grep -q gamma notes.txt ``` ### Merging when your tree has moved If your tree has changed since the split, `git apply --3way` can attempt a three-way merge. It stages the result and can leave conflicts. Commit or stage your own edits first. Two details matter: - `SPLIT_OBJECTS` holds file versions Git may not have. Pass it as `GIT_ALTERNATE_OBJECT_DIRECTORIES`. - Your index must match your working files. If Git reports a stale index after changes from the other side of the mount, run `git update-index -q --refresh`. The example keeps an uncommitted line from the starting snapshot and merges two later edits to different lines. The objects from the split are needed because that starting version was never committed to the original repository. ```sh set -e printf 'alpha\nbeta\nmiddle\nuncommitted\n' > notes.txt handle=$(marsh split -n -b fix='sed s/alpha/ALPHA/ notes.txt > next; mv next notes.txt') printf 'alpha\nbeta\nmiddle\nlocal\n' > notes.txt git add notes.txt marsh join -- sh -c 'cat >/dev/null export GIT_ALTERNATE_OBJECT_DIRECTORIES="$SPLIT_OBJECTS" git apply --3way "$SPLIT_DIR/fix/diff.patch"' <<<"$handle" grep -qx ALPHA notes.txt grep -qx local notes.txt ``` ## Inside marsh (Brush form) ```sh split { upper: tr a-z A-Z < notes.txt > n && mv n notes.txt count: wc -l < notes.txt } | join | less -FX ``` Each `label: command` line is one branch. Without a label, a branch is named after its command. - Labels start with a letter or `_`, followed by letters, digits, `_`, or `-`, up to 64 characters. They must be unique, ignoring case; `out`, `base`, and `owner` are reserved. - A split has at most 16 branches. - Commands after `join` read its output on stdin and run in your real project directory (in the marsh shell, the shell VM's view of it). They see `SPLIT_ID`, `SPLIT_DIR`, and `SPLIT_MANIFEST`, plus `SPLIT_OBJECTS` for Git projects. These variables are scoped to the downstream stages. - Every branch in `split { }` is a shell branch ([see below](#shell-branches-and--branches)). For `:::` branches, use the CLI form. ### Where one branch ends and the next begins A new branch starts after `{`, after a newline, or after `,`. After `;`, a new branch starts only if the next word is a label. A label matches `[A-Za-z_][A-Za-z0-9_-]*:`. Everything else is ordinary shell syntax inside the current branch. That includes other `;`, `&&`, `||`, `{ ...; }`, `( ... )`, and `case ... ;;`. | Input | Result | |---|---| | `a: cd x; make; b: cd y; make` | two branches, two commands each | | `x; y` | one branch | | `a: echo b: c` | one branch (only the first word after `;` counts) | | `x; echo: hi` | two branches, the second named `echo` | A command whose name ends in `:` is read as a label. Quote it to run it as a command: `'echo:' hi`. ```sh split { upper: tr a-z A-Z < notes.txt > n; mv n notes.txt; count: { wc -l; echo done; } < notes.txt } | join ``` ### What join prints For each branch, `join` prints a header with the status and where it ran (`shell-vm`, or `kit:NAME` for a `:::` branch), then its stdout, its stderr if it failed, and its diff. `-- LABEL: no changes --` means capture found no net changes relative to the snapshot. It does not describe new files excluded by ignore rules, shared-home writes, or edits the branch later reverted. A diff cut at the limit ends with `(diff truncated; full diff: PATH)`. A binary change appears as `binary file changed: PATH (N bytes; full patch in $SPLIT_DIR/LABEL/diff.patch)`. `diff.patch` has the full bytes. ### A fanout inside a branch A branch can run a [`fanout`](https://runmar.sh/docs/fanout.md) on its own fork. `join` shows the branch's `collect` output, and `marsh jobs --tree` draws a `fanout (...)` node under the branch: ```sh split { fix: claude -p "fix TestDialTimeout" review: fanout { races: codex exec "review net/ for races", leaks: claude -p "look for goroutine leaks" } | collect } | join ``` ## From plain bash (CLI form) ```sh marsh split -b upper='tr a-z A-Z < notes.txt > n && mv n notes.txt' \ -b count='wc -l < notes.txt' /.marsh/split//out manifest.json ($SPLIT_MANIFEST) each branch's state, status, and where it ran