Skip to content

Host operations

This page is for whoever runs the host process: the client that spawns muse serve, or a supervisor that keeps it running. It collects what that job needs in one place — what to pass the host, how to stop it, how to watch it, where it keeps its files, and the security model it runs under. The SDK spawn helpers already follow these rules; read on when you manage the process yourself or need to diagnose one.

muse serve is a Muse Session Protocol host over stdio. The process that spawns it owns its stdin and stdout and is its only connection: a host accepts exactly one client.

A host can load many sessions, but each session is loaded by one host at a time. The host holds a writer lease on every session it loads — a .session.lock file next to the session’s log, held with an operating-system file lock — so two hosts never write one session. Another host that needs the session is refused with sessionInUse, and the lease frees itself when its holder exits, crashes included.

Options are fixed for the host’s lifetime, so a client that needs two different settings runs two hosts. Run muse serve --help for the exact options your installed version accepts. They fall into four groups:

  • Durability. --no-session-log makes the host ephemeral: it writes nothing to disk, and its sessions end with the process. The handshake reports the choice as sessionDurability on the initialize result; see durability profiles.
  • Sandbox posture. The sandbox is on by default. --disable-sandbox, --sandbox-network <MODE>, --disable-write, --disable-shell, and --trust-workspace change what the agent may do on this host. The posture and the durability choice apply to every session the host loads, and no protocol method can change them; the security model below says what each boundary covers.
  • Provider and model. --provider and --model choose the startup provider and model for this process only. They never rewrite saved settings.
  • Configuration overrides. -c <KEY=VALUE> (long form --config) overrides one configuration value for every session the host loads. Keys under permissions are refused.

There is no approval flag. Approval mode is chosen per session over the wire — the mode field on session/start and session/setApprovalMode — from the modes the user’s configuration defines; see approvals.

If the host does not accept an argument, it exits 2 with a usage line on stderr, before anything is constructed.

The host runs with the environment it was spawned with. Both SDKs treat an environment you pass as the host’s whole environment, PATH included: extend your own environment rather than passing only the variables you add, or pass none to inherit yours. The option is env on MuseClientSpawnOptions in both the TypeScript and the Python SDK references.

HOME, XDG_CONFIG_HOME, and XDG_DATA_HOME decide where the host keeps its files; the table below shows how. MUSE_EXPERIMENTAL_SDK_ENABLED is an off-switch, so leave it unset: set to off, it makes every muse serve invocation exit 5 before reading its arguments.

Name each session’s working directory explicitly: pass an absolute workspaceRoot on session/start, or workspaceRoots, whose first entry plays the same role. That directory is where the session works. It is recorded with the session, reported as workspaceRoot on the session object, and restored when the session is resumed. Do not rely on the directory you spawned the host in to choose it.

A workspace root selects where a session works, never what the host may do there: the sandbox posture is still the one fixed at spawn.

The agent reads and writes the files on disk directly. An editor client with unsaved buffers should tell the user that the agent sees the saved version of each file.

Stop a host by closing its stdin. The host then cancels any active run, recording the turn as cancelled; finishes writing what was in flight; writes a session-end record for every session it has loaded; releases its leases; and exits 0. The whole sequence is bounded by a drain window, 30 seconds by default. Past it, the rest of the shutdown has crash semantics.

SIGTERM and SIGINT start the same orderly wind-down, so a service manager’s stop or a terminal’s Ctrl-C does not crash the host. When a signal starts the wind-down, the exit code names it: 143 for SIGTERM, 130 for SIGINT. Those codes have no row of their own in the exit-code list, so a client that classifies by exit code alone — the SDKs included — reads them as a crash, even though the sessions were closed in order. Close stdin when you hold the pipe.

A hard kill — SIGKILL, or the out-of-memory killer — ends the process with no session-end record. What the host already wrote stays: the next host that loads the session reconciles what was in flight.

The SDKs do all of this for you. close() closes stdin, waits out the drain window, then sends SIGTERM, and SIGKILL two seconds later if the host is still running. The wait is shutdownTimeoutMs in TypeScript and shutdown_timeout_ms in Python, 30 seconds by default.

  • Capture stderr, and never parse it. The host writes human-readable diagnostics there, and the format is not a contract. Keep it where a person can read it; the SDKs attach a bounded tail of it to every exit classification except a clean shutdown. See exit classification.
  • Branch on the exit classification, not on raw codes. Classify every way the host can exit shows the whole switch.
  • Restart only when a restart can help. Exits 2, 3, and 5 repeat until something outside the host changes: a client argument, the user’s configuration, or the build. Exit 4 clears when the process that holds the session exits.
  • After a durable host dies, respawn and resume. Its sessions survive; an ephemeral host’s do not. See survive the host dying under you.
What Default location Moved by
Settings and trust decisions ~/.config/muse/ XDG_CONFIG_HOME
The data directory: sessions, memory, snapshots, diagnostic logs ~/.local/share/muse/ XDG_DATA_HOME

HOME anchors both defaults. The host reports the data directory it uses as museHome in its initialize result: read it there rather than assuming the default.

Each session is a directory under the data directory’s sessions folder, partitioned by date:

<data directory>/sessions/<YYYY>/<MM>/<DD>/<session id>/session.jsonl
<data directory>/sessions/<YYYY>/<MM>/<DD>/<session id>/.session.lock

session.jsonl is the session: everything a client can learn about it is read back from this log. .session.lock is the writer lease. Every session object reports its log’s exact location as path; on an ephemeral host that is the empty string, because no log exists.

No protocol method deletes a session. Two housekeeping rules remove sessions on their own:

  • Expiry, when it is turned on. With session_retention_days set to a positive number of days, in the user’s settings or in managed settings, a session that has not been opened or used for that long — no turn and no resume — is deleted, folder and all. Without the setting, expiry is off and sessions are kept for good. Expiry never deletes a loaded session, and it runs on Unix hosts only for now.
  • Empty sessions. A session that holds nothing but its own start records is reclaimed during a later host’s startup housekeeping, once no host has it loaded; see survive the host dying under you.

Within a kept session, older history can be pruned behind compaction boundaries, and a read anchored in the pruned range gets a typed error that says where to start again; see resume. An ephemeral host writes no session files at all.

Besides stderr, the host keeps a private diagnostic log under <data directory>/local-tracing/: one file per process, readable only by your user, rotated as it grows. Each process deletes diagnostic files older than eight days when it starts. These files are for diagnosis, not a format to parse.

The spawn boundary. The host has no authentication, by design. Only the process that spawned it — or one that process handed the pipes to — can talk to it, and that party already holds your user’s authority: it could run the agent binary itself, edit your files, and run commands as you. So the host does not check it again. The clientInfo a client sends in initialize is for diagnostics, never for authorization.

Approvals bound the agent. What the agent may do without asking is set by the session’s approval mode, which a client selects from the modes the user’s configuration defines and can never author. A client may select even allowAll, which is safe only because of the spawn boundary. See approvals.

The sandbox bounds the agent’s commands. The sandbox posture is fixed when the host is spawned. The host checks once, at startup, whether it can enforce the sandbox, and reports the answer as sandboxAvailability in its initialize result, with a sandboxUnavailableReason when it cannot. If the sandbox is on but cannot be enforced, the agent’s shell commands fail with an environment error rather than run unsandboxed; running without the sandbox is the explicit --disable-sandbox choice.

Workspace content. --trust-workspace makes the host load each session workspace’s own skills and rules.

A shell as the user. A client granted the userShell capability can run a command with session/userShell. That command runs with the user’s own authority, outside the approval flow: the client asked, and it already holds that authority.