Skip to content

How to run a capsule from the CLI or from another program

mur run is the only way to start a capsule, but it has two output modes and they serve different callers. Interactively you want a session ID and a status line you can read. From a script, a CI job, or a supervising service you want the capsule's URL on stdout the moment it is reachable, so the caller can start sending it work. This guide covers both, plus the two read-only checks worth running before either.

The relevant manifest options are:

Option Controls
lifecycle.task_acceptance Whether the capsule runs task.md once or stays up to accept messages
network.internal_port A fixed port for the capsule's HTTP endpoint instead of an OS-assigned one
capabilities.containment The minimum kernel enforcement the host must provide before the capsule launches

Step 1 — write the manifest

Create a murmur.yaml file:

name: my-agent
version: "0.1.0"

artifacts:
  - name: murmur-driver-anthropic
    version: "1.0.0"
    runtime: driver

inference:
  transport: http
  endpoint: https://api.anthropic.com
  model: claude-sonnet-5
  api_key: ${ANTHROPIC_API_KEY}
  driver:
    artifact: murmur-driver-anthropic
name: my-agent
version: "0.1.0"

artifacts:
  - name: murmur-driver-openai
    version: "1.0.0"
    runtime: driver

inference:
  transport: http
  endpoint: https://api.openai.com
  model: o3-mini-high
  api_key: ${OPENAI_API_KEY}
  driver:
    artifact: murmur-driver-openai
name: my-agent
version: "0.1.0"

artifacts:
  - name: murmur-driver-deepseek
    version: "1.0.0"
    runtime: driver

inference:
  transport: http
  endpoint: https://api.deepseek.com
  model: deepseek-r1
  api_key: ${DEEPSEEK_API_KEY}
  driver:
    artifact: murmur-driver-deepseek

Create a task.md file next to it with the work you want done:

Summarise what this repository does in three sentences.

Step 2 — install dependencies

mur install
Different ways to install artifacts

mur install needs to know where to fetch artifacts from. You have two options:

Option A — configure a registry source in ~/.murmur/config.yaml:

registry:
  default: official
  sources:
    - name: official
      type: github
      repo: <owner>/<repo>
      token: "${GITHUB_TOKEN}"

Then install by artifact name and version:

mur install <artifact-name@version>

Option B — pass a full GitHub reference and skip configuration entirely:

mur install github:<username>/<repo>@<tag>

See Installing artifacts to learn more.


Step 3 — run it from the CLI

mur run --task task.md
murmur: url localhost:52222
session: ses_019ed2af53da75c2aefee84ee10c34af
status:  ok

The first two lines are printed the moment the capsule's HTTP port is bound, before the agent loop starts. The status: line is printed after the session ends.

Line Meaning
murmur: url Where the capsule is reachable while it runs
session: The session ID — names the workdir subdirectory and appears in trace.jsonl
status: ok, failed, or trapped

--task also accepts inline text. If the value is the path to an existing file its contents are copied to task.md; anything else is written to task.md verbatim:

mur run --task "Summarise what this repository does in three sentences."

Add -v when you need the paths and the resolved identity:

mur run --task task.md -v
murmur: url localhost:52222
session: ses_019ed2af76b57c42912b612f07dd4d51
workdir: /path/to/workdir/ses_019ed2af76b57c42912b612f07dd4d51
manifest: my-agent v0.1.0
driver: murmur-driver-anthropic (claude-sonnet-4-6)
skills: 2 installed
status:  ok

The driver: line names the artifact and model for transport: http, or the command and model for transport: process. The skills: line appears only when the manifest declares at least one runtime: skill artifact.


Step 4 — run it from another program

Pass --json to get a single JSON line on stdout instead of the human-readable lines. It is written as soon as the HTTP server is listening, so a caller can block on that one line and then start sending work:

mur run --json --task task.md
{"url":"localhost:52222","pid":12345,"session_id":"ses_019ed2af76b57c42912b612f07dd4d51","name":"my-agent","version":"0.1.0","workdir":"/path/to/workdir/ses_019ed2af76b57c42912b612f07dd4d51"}
Field Type Description
url string localhost:PORT — the capsule's HTTP endpoint
pid number The mur process ID, for waiting on or killing the run
session_id string Matches the session_id field in the session's trace.jsonl
name string Capsule name from the manifest
version string Capsule version from the manifest
workdir string Absolute path to the directory the agent can read and write

--json takes precedence over -v: when both are set, no human-readable output is produced at all.

If the run fails before the port is bound — a bad manifest path, a missing artifact — stdout stays empty. The error goes to stderr and the exit code is non-zero, so a caller that reads one line from stdout can treat "no line" as a launch failure.

Connect with retry and backoff. A small window exists between the port being bound and the server accepting the first request.

Send the capsule work

A capsule that runs task.md and exits needs nothing further. To keep it up and drive it over HTTP, set lifecycle.task_acceptance to single or queue and read the port out of the JSON line:

PORT=$(mur run --json --task task.md | head -n 1 | sed 's/.*localhost:\([0-9]*\).*/\1/')

The capsule describes itself at /.well-known/agent-card.json:

curl -s http://localhost:$PORT/.well-known/agent-card.json

Send it a message over JSON-RPC:

curl -s -X POST http://localhost:$PORT \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "messageId": "msg-001",
        "role": "user",
        "parts": [{"text": "Summarise the README."}]
      }
    }
  }'

See Connect two capsules with A2A messaging for the full request and response shapes.

Two flags matter when something other than a local script is the caller:

Flag Use it when
--bind 0.0.0.0 The caller is on another machine. The printed url stays localhost:PORT — substitute the host address yourself
--no-env-file Running in CI. It skips auto-loading the workspace-root .env, so secrets come only from the environment you injected

For a port that does not change between runs, declare network.internal_port in the manifest. The runtime then binds that exact port and fails with error[E-RUN-010] if it is already taken, instead of picking a free one.


Step 5 — inspect the capsule's reach before launching it

--explain-scope resolves the capsule's effective grants and prints them, then exits 0. It contacts no registry, compiles no component and creates no workdir, so it is safe to run against any manifest:

mur run --explain-scope
Containment
  declared:  advisory
  achieved:  scoped
  floor met: yes
  mechanism: landlock+seccomp
  userns grant: profile_confining

Effective grants
  filesystem scope: <none>
  workdir exec:     false
  read_only: <none>
  preopens:
    - murmur-tool-git (tool): the whole accessible workdir — no capabilities.filesystem.scope declared
    - murmur-driver-anthropic (driver): the whole accessible workdir — no capabilities.filesystem.scope declared
    - murmur-hook-telemetry (hook): nothing preopened — no capabilities.filesystem.scope declared
  network allow: <none>
  unix sockets:     false
  shell allow: <none>
  spawn allow: <none>
  env allow: <none>
  plan submit:      false
  interpreter runtime: <none>
  staged runtime: <none>

The report shows declared network destinations, not resolved IP addresses — it deliberately skips DNS to stay fast and read-only.

preopens: lists one line per tool, driver and hook: which directory that artifact works out of once its own capabilities.filesystem.scope is applied. A tool or driver that declares no scope gets the whole accessible workdir; a hook that declares none gets no directory at all. See The filesystem default for why the two roles start from opposite baselines and when to narrow one.

userns grant: names where this host's permission to create an unprivileged user namespace comes from, and is n/a off Linux. See Where the user namespace comes from for the values.

mechanism: is a stable name for the kernel enforcement the host actually provides:

mechanism Achieved class What the host provides
mountns+pivot_root+landlock+seccomp sealed Private mount namespace pivoted onto a composed root, with Landlock and seccomp inside it
landlock+seccomp scoped Landlock filesystem mediation plus the seccomp syscall allowlist over the host filesystem
seccomp-only advisory Linux without a usable Landlock ABI: seccomp only, filesystem scope by convention
none advisory No kernel sandboxing primitive (macOS and every non-Linux target)

Add --json for one machine-readable line instead, which is the same object trace.jsonl records as effective_grants on every session:

mur run --explain-scope --json

Because it is a diagnostic, it reports even when the capsule could not launch here — which is exactly the case you are inspecting. When the floor is not met it says so and exits 0 anyway:

This is a report only — `mur run` without --explain-scope would refuse to launch here.

Step 6 — require a containment floor

A containment class is a floor: "do not launch me unless the host can enforce at least this much". Declare it in the manifest when the capsule should never run unprotected:

capabilities:
  containment: scoped

Or require it for one invocation:

mur run --task task.md --containment scoped

Three sources can each declare a floor — the manifest, containment in .murmur/config.yaml, and the flag — and they combine by taking the strongest. The flag can raise a floor the manifest set, never lower it.

mur run then probes the kernel directly rather than trusting the manifest. If the host falls short, the run is refused before any registry pull, component compile, or workdir creation:

error[E-CAP-003]: declared containment class 'scoped' is not achievable on this host (achieved: 'advisory'): scoped requires Landlock filesystem mediation (Linux 5.13+ with a usable Landlock ABI); this host provides no kernel filesystem mediation, so paths outside the workdir are constrained by convention only
  hint: lower the declared floor to 'advisory' (capabilities.containment in murmur.yaml, containment in .murmur/config.yaml, or --containment), or run on a host that provides 'scoped'

A manifest that declares nothing is never gated by this check — the effective floor is advisory, which every host satisfies. See Containment for what each class enforces and E-CAP-003 for the other refusal reasons.


Summary

Feature / setting How it works
mur run --task task.md Human-readable: URL and session ID at port bind, status: when the session ends
--task <text> An argument that is not an existing file path is written to task.md verbatim
mur run -v Adds workdir:, manifest:, driver:, and skills: to the startup lines
mur run --json One JSON line at port bind carrying url, pid, session_id, name, version, workdir
--json with -v --json wins; no human-readable output is produced
Launch failure with --json Empty stdout, error on stderr, non-zero exit
--bind 0.0.0.0 Accepts connections from other machines; the printed url still reads localhost:PORT
--no-env-file Skips the workspace-root .env; the recommended default in CI
network.internal_port Binds one fixed port; error[E-RUN-010] when it is already taken
mur run --explain-scope Prints declared and achieved containment plus every effective grant, then exits 0 without staging anything
--explain-scope --json The same report as one line, identical to effective_grants in trace.jsonl
capabilities.containment / --containment Strongest of manifest, workspace config, and flag; a host that falls short refuses with E-CAP-003