Command registry
The registry maps the command a user types to a native Kit v3 source or immutable reference. The Kit itself owns the workload configuration:
{
"claude": "kits/marsh-claude",
"codex": "kits/marsh-codex",
"pi": "kits/marsh-pi",
"shell": "kits/marsh-shell"
}
Release packaging installs libexec/marsh/commands.json. An optional host-control commands.json overlays packaged entries by command name. packaging/commands.json names local authoring sources. For a releasable candidate, make kit-publish KIT_REPOSITORY_PREFIX=docker.io/YOU builds and pushes each native v3 Kit through Buildx, then writes target/kit-release/commands.json with immutable OCI manifest references. Install that file with make install KIT_COMMANDS=target/kit-release/commands.json. Both stock SBX and the worker's private Docker Engine then pull the same published image. Stock SBX must allow the registry in kit.allowedSources.
The loader and live kit install use the same declaration rules in marsh-contracts::command_registry. These rules are declared in crates/marsh-contracts/src/command_registry_rules.json and embedded by Rust. A registry has at most 251 commands, and each registry file is at most 1 MiB. Names are 1–128 ASCII bytes, containing letters, digits, _ or -, and cannot start with -. Reserved names are marsh, fanout, collect, acp, mcp, ps, and top. Public MCP tool/publication names are a different semantic type; their dot syntax does not make dots valid Kit command names.
The loader accepts a JSON object whose keys obey those rules. Each value is either a native v3 source directory or an immutable repository@sha256:<64 lowercase hex> OCI reference. Relative directories are resolved from the registry file, so packaged kits/marsh-claude names the installed source while the same value in host-control commands.json resolves relative to that host-control directory. Use an absolute path to a Kit under $MARSH_HOME/kits/. Directories are canonicalized and must exist. Duplicate keys (including escaped spellings of the same key), path-like or empty command names, mutable image tags, non-string values, and malformed documents are rejected. Registry files must be regular files, not symlinks or FIFOs. Both declarations and their combined size are validated before any Kit resolution or stock call. A host-control entry explicitly overrides a packaged entry of the same name; an empty {} retains packaged commands.
Keep every registered Kit source outside the project and selected guest home. The daemon protects these trusted sources from guest writes and rejects a project/home mount that contains a source directory or is inside one. To edit a Kit in the project, copy or install its candidate source into a separate host-owned directory, then map that snapshot. When developing the packaged Kits in this repository, make install copies them outside the checkout; see the self-development workflow.
The referenced native workload is the sole authority for the image config, entrypoint, command, static environment, user, workdir, capabilities, network policy, and services. marsh supplies only invocation arguments plus the host/session bindings required by its product contract: nonroot Mac UID/GID, natural working directory and home path, selected home backing, terminal mode, and exact project/home grants.
Directories under kits/ are native v3 source for stock Docker tooling. For a local-source Kit, marsh builds the job image with Buildx while stock SBX builds the Kit VM's image from the same source, so the two builds are independent. On a cold Kit VM, marsh requires the source fingerprint to be unchanged, the host build's manifest digest to equal the Kit VM's image_digest, and the image loaded into the VM to have that same ID. On a digest mismatch it has stock SBX rebuild the Kit once, then fails with still mismatched after stock SBX rebuilt the Kit (marsh-sbx prepare_cold_local_kit); the two builds agree only when they share a BuildKit cache. Use published digests for packaged acceptance and release; local-source mappings remain an authoring path without a qualification claim.
For local v3 source, stock SBX derives Kit identity from the source directory basename. Packaged sources therefore use the marsh-* namespace so they do not collide with stock built-in agent identities. This identity is independent of the command registry key: users still invoke claude, codex, pi, and shell.
The command is registered in Brush through a generic process shim. The shell also prepends a private, session-lived directory of command links to exported PATH. Descendants such as /bin/sh, scripts, make, and agent subprocesses that resolve a registered name through PATH therefore return to the same daemon and create a fresh container. Brush functions and builtins retain their normal precedence, and explicit executable paths bypass name resolution. Every foreground, pipeline, background, or concurrent invocation returns to the shared daemon and creates a new container. The shell command name has no hard-coded provider behavior. The generic shell workload uses /bin/sh as its native image entrypoint. A bare noninteractive invocation reads stdin as a script, shell -c ... passes the remaining argv directly to /bin/sh, and a bare PTY invocation is an interactive shell. The normal worker lifecycle gives each call a fresh container while reusing the exact shell Kit VM.
To add a published alternate Kit while the daemon is running, use marsh kit install NAME --from repository@sha256:DIGEST. The daemon checks the immutable reference through stock SBX, prepares its worker, and then registers the command in its current scope. It writes the pinned reference to that scope's private control registry for the next daemon start. Inside an attached project shell, the new name becomes available on PATH immediately. Failed preparation leaves the registry unchanged. Installation rejects an existing name rather than treating it as an override. It validates the existing control registry before any stock preparation or file write; malformed or duplicate-key configuration must be fixed, not silently rewritten. Concurrent installs are serialized, and a host edit detected during preparation is not overwritten.
After changing a registry entry, local Kit source, or stock-SBX service secret, the host can discard idle cached workers with marsh workers reset KIT[,KIT...] or marsh workers reset all, then recreate them with the matching marsh --load selection. Reset resolves names through this registry, acts only on Kit VMs in the daemon's ownership map with their recorded UUID, refuses any selected worker with an active reservation or container, and removes only those Kit worker VMs. A quarantined Kit VM is retired the same way; the next use creates a new VM with a new random name. It does not remove the project shell VM, selected home, receipts, authentication state, or unrelated Docker Sandbox VMs. Guest relay tokens are not authorized for this host lifecycle operation.
ACP adapter configuration
agents.json has different overlay semantics: a host-control file replaces the complete packaged adapter set. An explicit [] disables all ACP adapters; omitting the file selects packaged defaults. A nonempty replacement must list every adapter the user wants to keep. No implicit merge resurrects a packaged adapter after an explicit disable. Duplicate adapter names or fields, invalid declarations, missing referenced commands, or mismatched Kit generations are fatal startup configuration errors, not a silent fallback to disabled ACP. The startup diagnostic names the configuration paths without echoing contents. These checks happen before stock version/preparation calls. The same regular-file and 1 MiB input bounds apply.
Each declaration names a registered command and may add a fixed arguments tail (at most 8 printable values of at most 128 bytes) that the daemon appends to that Kit's own entrypoint when the session starts, exactly like typed arguments. The packaged agents use it to select ACP mode in the one-per-agent Kits:
{"schema_version":1,"name":"claude-session","protocol":"acp_v1","command":"claude","arguments":["--acp"]}
The tail cannot change the Kit's image, entrypoint, mounts, network, or credentials.
The host-control directory is scoped by the SHA256 of the canonical MARSH_HOME path under ~/Library/Application Support/marsh/control/. MARSH_CONTROL_HOME selects another private control root with the same per-scope suffix. This directory is never mounted as the guest home.