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:
shell -c 'uname -sm'
shell is a packaged command. To add your own, use marsh kit install NAME --from REPOSITORY@sha256:DIGEST, substituting its published reference. marsh uses the native Docker Sandboxes Kit v3 format.
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 |
Releases pin the images by digest (docker.io/mcavage/marsh-*@sha256:...). The list is in the installed libexec/marsh/commands.json.
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.
Updating agent versions
Kit images pin the agent CLI version. To get the images selected by a newer marsh release, upgrade marsh on your Mac. For Homebrew:
brew upgrade mcavage/tap/marsh
Finish jobs, exit marsh shells, and run marsh stop before reopening. 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. To choose an agent version independently of marsh releases, register your own Kit.
How a call runs
When you invoke a registered command:
- The shell asks the marsh daemon on your Mac to start a job.
- The daemon finds a warm VM for the command's Kit, or creates one.
- Inside that VM, a fresh container starts from the Kit image. It runs as a nonroot user with your UID.
- 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
$HOMEin both cases. - 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).
- 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 8 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). Use split 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. 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 | 4 |
| Memory | 8 GiB |
| Processes | 4,096 |
| Writable layer | 10 GiB |
| Output | 256 MiB |
| Wall time | 24 hours |
To change these, see Configuration.
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.
Adding a command
marsh kit install NAME --from REPOSITORY@sha256:DIGEST validates and registers a published Kit through sbx. Open shells see the name immediately, and the mapping persists for later launches. The reference must be pinned by digest. An existing name, including a packaged one, is refused. To replace a name, edit commands.json below.
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:
{
"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);
- 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:
# 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:
HOMEset to the mounted guest home. The same path is inMARSH_SELECTED_HOME./run/marsh/binfirst onPATH. It holds the registered commands and an in-jobmarsh./run/marsh/context.md, a short text telling an agent what it may start.
Keep tool state under $HOME so it persists between calls. Other jobs in the scope can read it.
The packaged Kits are in kits/. They show how the agent Kits pass context.md to their agent.
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).
To share a Kit, post it in Show and tell.
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).
Next: Troubleshooting, Configuration.