Configuration
Configuration files
mur reads up to two YAML files and resolves them into one effective config:
| File | Scope | Discovery |
|---|---|---|
~/.murmur/config.yaml |
Global (per-user) | Fixed path |
<cwd>/.murmur/config.yaml |
Project (per-workspace) | <cwd> only; parent directories are not searched |
Both files are optional; a missing file is treated as empty. Write them with
mur config set (project by default, -g for global) or edit the YAML by
hand.
Merge rules
Where both files set a value, the effective config is built per field:
| Field | Rule |
|---|---|
registry.default |
Project wins if non-empty, else global |
registry.index_url |
Project wins if non-empty, else global |
inference.provider, inference.model, inference.endpoint |
Project wins if non-empty, else global |
inference.api_key |
Always the global value — see inference.api_key is always global |
credentials |
Always the global map. A non-empty credentials: in the project file is ignored, with a warning naming that file — see credentials: section |
registry.sources |
Union by name: a project entry sharing a global entry's name replaces it in place (position preserved); a project entry with a new name is appended; global-only entries are never dropped |
beta.enabled |
Union by value: global flags first, then any project-only flags appended, in the project file's order |
containment |
Strongest wins: a project file may raise the class the global file asked for, never lower it. See Containment class |
spend.machine_tokens_per_day |
Lowest wins: a project file may lower the ceiling, never raise it. See spend: section |
The base of the merge is the global file, or the built-in default when
~/.murmur/config.yaml is absent. That default is registry.default: official and a single
GitHub source, murmur-nexus/default-artifacts. Once the global file exists it is the base in
full: a key it omits is empty, not defaulted, so a global file that declares no
registry.sources leaves mur install with an empty source chain.
inference: section
Which inference provider mur uses, and the credentials and endpoint to reach it with.
mur new reads this block from ~/.murmur/config.yaml directly rather than
from the effective config, and uses it only when it is complete: provider is anthropic or
openai, and both model and api_key are non-empty. An incomplete block is skipped, and
mur new falls back to the ANTHROPIC_API_KEY / OPENAI_API_KEY environment variables and
then the interactive wizard.
inference:
provider: anthropic # "anthropic" or "openai"
model: claude-haiku-4-5-20251001
api_key: sk-ant-...
endpoint: "" # optional; leave empty for the provider default
| Key | Required | Description |
|---|---|---|
provider |
yes | anthropic or openai |
model |
yes | Model name to request from the provider |
api_key |
yes | API key for the provider |
endpoint |
no | Base URL of the provider's API, for a proxy or a compatible service. Empty selects the provider default |
An empty endpoint resolves per provider:
| Provider | Default endpoint |
|---|---|
anthropic |
https://api.anthropic.com |
openai |
https://api.openai.com |
inference.api_key is always global
inference.api_key is the one field that does not follow "project wins": the effective config
reads it from ~/.murmur/config.yaml only, whatever the project file contains, and whether that
value is a literal or a ${VAR} reference. If the global file has no inference: block at all,
the effective api_key is "".
A literal inference.api_key in the project file triggers a warning — both when the
effective config is loaded and when mur config set writes it:
warning: <cwd>/.murmur/config.yaml sets inference.api_key to a literal value, but inference.api_key is always read from the global config (~/.murmur/config.yaml); this project-level value will be ignored
A ${VAR} reference prints no warning. The variable name must be uppercase letters, digits and
underscores, starting with a letter or underscore — ${MY_ORG_KEY} is a reference, ${my_key}
is a literal and warns.
mur run does not read inference.api_key. It reads provider keys from
credentials:, and mur config set inference.api_key prints a one-line note saying
so.
credentials: section
Provider keys, by credential name. A manifest's
inference.api_key: ${NAME} names a credential, and mur run
reads its value from credentials.NAME here.
credentials:
ANTHROPIC_API_KEY: sk-ant-...
OPENAI_API_KEY: sk-...
| Key | Type | Description |
|---|---|---|
<NAME> |
string | The key for credential NAME. NAME is uppercase letters, digits and underscores, starting with a letter or underscore. An empty value counts as absent |
Write an entry with mur config set -g. Only the global file holds credentials, so the command
refuses without -g, and it prints the key's name, never its value:
mur config set -g credentials.ANTHROPIC_API_KEY sk-ant-...
# Set credentials.ANTHROPIC_API_KEY in ~/.murmur/config.yaml
Where ${NAME} is read from
| Situation | Key used | Re-read while the capsule runs |
|---|---|---|
credentials.NAME is a non-empty entry in ~/.murmur/config.yaml |
That entry, even when the environment variable NAME is also set |
Yes |
No entry, and the environment variable NAME is set |
The variable, with W-SEC-027 |
No |
| Neither | None: mur run refuses before any session directory exists, with E-MAN-003 naming both places |
— |
inference.api_key is a literal, not ${NAME} |
The literal, with W-SEC-027 |
No |
No inference.api_key |
None | — |
Rotating a key
A replaced entry takes effect on the next inference request that any running capsule sends. Nothing restarts. Before each request that carries the key, the runtime reads the entry from the config file, however the file was last written.
| Change | When a running capsule uses it |
|---|---|
mur config set -g credentials.NAME <key>, or any hand edit, in place or by rename |
The next request that reads the file after the write completes. This is the worst case for every way of writing the file |
| A request that reads the file while an editor is part-way through saving it | That request uses the last key read, and the trace records unreadable. The next request uses the saved key |
| Removing the entry, or the file becoming missing or unparseable | Never. The capsule keeps the last key it read, and the trace records change: "unreadable" once for that state of the file |
Removing an entry does not revoke a running capsule's key; replacing it does.
When the provider answers 401, the runtime reads the file again before deciding whether to
resend. A changed value is sent in one resend of the same request, and that response goes to the
driver whatever its status. An unchanged value is not resent. A 401 that stands ends the task with
E-RUN-027. Every rotation and rejection is recorded in the session
trace as an inference_credential event, which
never carries the key.
A capsule started by another capsule's delegate-task runs with the same HOME, so it reads the
same file and picks up a rotated key too. Only transport: http capsules read credentials.
File permissions
Every write to the global file leaves ~/.murmur/config.yaml mode 0600 inside a ~/.murmur held
at 0700, whatever the umask and whatever modes the two had before. A write that cannot set either
mode fails with E-IO-003. The project-level <cwd>/.murmur/config.yaml is written under the
umask; it cannot hold credentials.
| Writes the global file | Command |
|---|---|
| Keys and credentials | mur config set -g |
beta.enabled |
mur beta enable, mur beta disable |
inference: |
The mur new first-run setup |
A file loosened after it was written stays loose until the next write, and is reported:
| Reports it | When | Output |
|---|---|---|
mur run |
A transport: http capsule reads its key from credentials.<NAME> in a file that grants any group or other permission |
W-SEC-028 on stderr, once per launch |
mur doctor |
Always | The mode of every entry in ~/.murmur, and W-SEC-028 for each one wider than the table below — see mur doctor |
Neither changes a mode or refuses. Tighten the file by hand:
chmod 600 ~/.murmur/config.yaml
~/.murmur modes
| Path | Mode | Holds | Written by |
|---|---|---|---|
~/.murmur/ |
0700 |
Everything below | Held by every writer below before it creates anything |
config.yaml |
0600 |
Provider credentials, registry tokens | mur config set -g, mur beta, mur new |
state/, state/<store>/ |
0700 |
Capsule state stores. Files inside are written by the capsule with no mode set | mur run at staging |
spend/, spend/<YYYY-MM-DD>.jsonl |
0700, 0600 |
The machine spend ledger | mur run with spend.machine_tokens_per_day in effect |
conversations/ and each directory below, conversation.jsonl |
0700, 0600 |
Conversation records | mur run, on the first recorded message; mur conversation rewrites |
running/, running/<session_id>.json |
0700, 0600 |
Running-capsule records | mur run when a session opens its door |
deployments.json |
0600 |
Deployment records | mur deploy, mur destroy |
deploy_staging/, deploy_staging/<deployment_id>/ |
0700 |
A copy of the manifest, workdir and mur binary while a deploy uploads |
mur deploy |
deploy_keys/ |
Expected 0700, files 0600 |
SSH private keys for a deployment | Nothing writes here; mur destroy removes a deployment's directory |
artifacts/ |
Umask | Installed artifacts | mur install, mur publish |
bin/mur-* |
0755 |
Cached mur binaries for deploy targets |
mur deploy |
Every mode marked 0700 or 0600 is set again on each write, not only when the path is created.
A mur run that writes under an existing ~/.murmur removes its group and other permissions and
leaves its owner permissions as they are, so a home closed with chmod 500 stays closed.
registry: section
| Key | Description |
|---|---|
default |
Name of the sources entry tried first when resolving an artifact by name. Built-in default: official |
index_url |
Artifact index mur search fetches. See Artifact index and custom registry URL |
sources |
Sources mur install walks in order. See Multiple sources and fallthrough |
beta: section
The beta features opted into with mur beta enable / mur beta disable. Those commands read and
write the global file only, and beta.enabled is not a mur config set key
— a project-level entry can only be added by editing <cwd>/.murmur/config.yaml by hand.
beta:
enabled:
- mur-new
- mur-deploy
| Key | Type | Description |
|---|---|---|
enabled |
array of strings | Feature names opted into. An absent beta: section is equivalent to enabled: [] |
A name this build does not compile in has no effect until a build that includes it is installed.
mur beta list prints the features this build has, and mur beta enable
warns when the name is not one of them.
spend: section
A machine-wide ceiling on inference spend, counted in tokens per UTC day.
spend:
machine_tokens_per_day: 5000000
| Key | Type | Required | Description |
|---|---|---|---|
machine_tokens_per_day |
integer | no | Most tokens every run on this ~/.murmur may spend per UTC day. No default: absent sets no machine ceiling. 0 is refused with E-IO-003 when the config is loaded |
It counts what inference.max_session_tokens counts:
the runtime's own input_tokens + output_tokens for every driver call, agent turns and hooks'
run-inference calls alike. Before each call, a run adds the day's total, its own calls still in
flight and the call's input plus its most output; a call that would cross the ceiling is refused
before it is sent, with limit: "machine" on its
spend_ceiling_reached line. A machine refusal
does not latch: the next call is checked again, and the total starts from zero at 00:00 UTC.
The ceiling is approximate. Each run appends a call to the shared ledger when the call settles,
and checks the ledger before its next call. A call that is admitted and not yet settled is
invisible to every other run. The machine total can therefore exceed
spend.machine_tokens_per_day by at most the tokens of the calls in flight on the machine
(admitted and not yet settled) when the last call was admitted. Those tokens are the runtime's own
input_tokens + output_tokens for each call, however much output the call asked for.
Calls that can be in flight at the same time:
- one agent turn per running session
- one agent turn per delegated child, since each child is a
session of its own and keeps running after
delegate-taskreturns - one
run-inferencecall perexecution_mode: asynchook, which runs alongside the agent turn
The bound holds only while the ledger can be read and appended to. A run that cannot append to it prints a warning, and those calls are not counted.
| Covered | Not covered |
|---|---|
Runs whose HOME shares this ~/.murmur — in practice, one user account |
Other user accounts on the same host |
Sessions started by mur run, including delegated children, with the ceiling in effect |
Sessions started by mur eval run or mur new, and any run launched without the ceiling in effect: they write no ledger lines and are not counted |
transport: http capsules |
transport: process capsules, whose CLI reaches its provider with its own credentials — see W-SEC-026 |
A ~/.murmur on a local filesystem |
A ~/.murmur on NFS, where the atomicity of appends the ledger relies on does not hold |
Each run enforces its own effective config's value against the shared total, so two runs launched from projects with different ceilings each stop at their own.
The ledger is one file per UTC day:
| Property | Value |
|---|---|
| Path | ~/.murmur/spend/<YYYY-MM-DD>.jsonl |
| Modes | Directory 0700, file 0600 |
| Line | {"ts":<unix ms>,"session_id":"ses_…","input_tokens":<n>,"output_tokens":<n>} |
| When written | Once per settled call. A call cancelled, refused at dispatch or failed is written with its input_tokens and output_tokens: 0 |
| Retention | A run that opens the ledger removes <YYYY-MM-DD>.jsonl files dated more than 30 days before today. Other names are left alone |
A run that cannot create or open the ledger refuses to start with
E-RUN-026.
Where the effective config is used
| Consumer | Reads |
|---|---|
Beta gating — which beta subcommands mur --help lists, and mur beta list's enabled column |
beta.enabled |
mur install source-chain resolution |
registry.default, registry.sources |
mur search |
registry.index_url |
mur run, mur doctor — the containment floor |
containment |
mur run — the key for inference.api_key: ${NAME}, re-read while the capsule runs; mur doctor — whether to warn with W-SEC-027 |
credentials, from the global file only |
mur run — the machine spend ceiling; mur run and mur doctor — W-SEC-026 |
spend.machine_tokens_per_day |
mur new and mur deploy read ~/.murmur/config.yaml only; a project-level file does not
affect them.
Artifact index and custom registry URL
mur search fetches a static JSON catalog, artifacts-index.json. The default is the copy in
the Murmur default-artifacts repository:
https://raw.githubusercontent.com/murmur-nexus/default-artifacts/refs/heads/main/artifacts-index.json
To point mur search at a different catalog — a private org index, say — set
registry.index_url in ~/.murmur/config.yaml, or in <cwd>/.murmur/config.yaml to scope it to
one project:
registry:
index_url: https://my-org.example.com/artifacts-index.json
registry.index_url applies to every mur search invocation that does not pass --registry.
artifacts-index.json shape:
{
"schema_version": "1",
"updated_at": "2026-06-07T00:00:00Z",
"artifacts": [
{
"name": "murmur-tool-git",
"version": "1.0.0",
"runtime": "tool",
"description": "Structured git interface for Murmur capsules.",
"tags": ["tool", "git"],
"platforms": ["darwin-aarch64", "linux-aarch64", "linux-x86_64"]
}
]
}
| Field | Type | Required | Notes |
|---|---|---|---|
schema_version |
string | yes | Must be "1". Any other value fails the search with E-IO-003 |
updated_at |
string | yes | ISO 8601 UTC timestamp of the last regeneration |
artifacts |
array | yes | One entry per published artifact |
name |
string | yes | Artifact name, matching name: in its murmur.yaml |
version |
string | yes | SemVer string |
runtime |
string | yes | driver, hook, tool, or skill |
description |
string | no | Short description from murmur.yaml. mur search matches the query against it and prints an em dash when it is absent |
tags |
array[string] | no | Keyword tags matched against the query. Defaults to empty |
platforms |
array[string] | no | e.g. darwin-aarch64. Defaults to empty; skill artifacts have none |
Registry selection rules
mur install and mur publish resolve artifacts against either the local registry under
~/.murmur/artifacts/ or a remote Nexus registry, chosen in this order:
--registry <value>—local(case-insensitive) selects local mode; any other value is the remote URL.registry.remote_urlinmurmur.yaml— remote mode at that URL.registry.defaultinmurmur.yaml—localorremote;remoteuseshttp://localhost:7800. Any other value fails withE-IO-003.- Local mode.
These two keys live in the workspace manifest murmur.yaml. Its registry.default takes local
or remote, unlike registry.default in .murmur/config.yaml, which names a sources entry.
Remote mode requires the NEXUS_API_KEY environment variable; without it the command fails with
E-IO-003 and the message NEXUS_API_KEY is required for remote registry mode. Set it or use
local mode.