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 |