Muse Low-level Commands Reference
Low-level commands are the machine-readable layer of the Muse CLI. They output JSON, stream bytes without size limits, use predictable exit codes, and compose cleanly in shell pipelines and agent scripts.
If you want to automate Muse — write a script, build an agent workflow, or
integrate Muse into another tool — these commands are the right entry point.
The higher-level porcelain commands (muse commit, muse merge, etc.) call
these internally.
These commands were previously namespaced under muse plumbing <cmd> and are
now top-level: muse <cmd>. No sub-namespace needed.
Quick Index
| Command | Purpose |
|---|---|
hash-object |
Compute SHA-256 of a file; optionally store it |
cat-object |
Stream raw bytes or metadata for a stored object |
verify-object |
Re-hash stored objects to detect corruption |
rev-parse |
Resolve branch name / HEAD / prefix → full commit ID |
read-commit |
Print full commit JSON record |
read-snapshot |
Print full snapshot JSON record |
ls-files |
List tracked files and their object IDs |
commit-tree |
Create a commit from an existing snapshot |
update-ref |
Move or delete a branch ref |
commit-graph |
BFS walk of the commit DAG |
merge-base |
Find the lowest common ancestor of two commits |
snapshot-diff |
Diff two snapshots: added / modified / deleted |
pack-objects |
Pack commits, snapshots, and objects into an MPack |
unpack-objects |
Apply an MPack to the local store |
verify-pack |
Three-tier integrity check for an MPack |
show-ref |
List all branch refs and their commit IDs |
symbolic-ref |
Read or write the HEAD symbolic reference |
for-each-ref |
Iterate refs with full commit metadata; sort and filter |
name-rev |
Map commit IDs to <branch>~N names |
check-ref-format |
Validate branch/ref names against naming rules |
check-ignore |
Test whether paths match .museignore rules |
check-attr |
Query .museattributes for merge strategies |
domain-info |
Inspect the active domain plugin and its schema |
ls-remote |
List refs on a remote without changing local state |
The Plumbing Contract
Every plumbing command follows the same rules:
| Property | Guarantee |
|---|---|
| Output | Text (or binary, for pack-objects) by default; JSON with --json |
| Exit 0 | Success — output is valid and complete |
| Exit 1 | User error — bad input, ref not found, invalid ID |
| Exit 3 | Internal error — I/O failure, integrity check failed |
| Idempotent reads | Reading commands never modify state |
| Idempotent writes | Writing the same object twice is a no-op |
| Encoding | All text I/O is UTF-8 |
| Object IDs | Always the canonical sha256:<64 lowercase hex> form |
| Short flags | Every flag has a -x short form |
Every command accepts a boolean --json / -j flag. There is no
--format flag anywhere in the CLI — flags are booleans, not
choice-style value flags. Passing --json switches output from plain text
(the default for most commands) to structured JSON; omitting it prints the
human-readable default shown in each command's "default output" example
below. Check each command's own section — some (ls-remote, show-ref,
read-snapshot, verify-object, domain-info, symbolic-ref,
for-each-ref, name-rev, check-ref-format, check-ignore, check-attr)
default to plain text and require --json for structured output, matching
git-style plumbing conventions.
JSON output is always printed to stdout. When an error occurs in --json
mode, the error is emitted as a {"error": "..."} (or {"status": "error", "error": "..."}) object on stdout — not stderr — so scripts that parse
stdout can detect the failure without inspecting exit codes separately.
Every --json response is wrapped in a standard envelope carrying
muse_version, schema, exit_code, duration_ms, timestamp, and
warnings, in addition to the command-specific fields shown in each example
below. The examples in this document omit the envelope for brevity and show
only the command-specific fields — the envelope keys are always present
alongside them. For example, muse rev-parse HEAD --json actually prints:
{"muse_version": "0.2.1rc4", "schema": 1, "exit_code": 0, "duration_ms": 0.9,
"timestamp": "2026-03-18T12:00:00.000Z", "warnings": [],
"ref": "HEAD", "commit_id": "sha256:a3f2...c8d1"}
Command Reference
hash-object — compute a content ID
muse hash-object <file> [--stdin] [-w] [-j]
Computes the SHA-256 content address of a file or stdin stream. Identical
bytes always produce the same ID; this is how Muse deduplicates storage.
With --write (-w) the object is also stored in .muse/objects/ so it can
be referenced by future snapshots and commits. The file is streamed at
64 KiB at a time — arbitrarily large blobs never spike memory.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--stdin |
— | off | Read content from stdin instead of a file path |
--write |
-w |
off | Store the object after hashing |
--json |
-j |
off | Emit machine-readable JSON (default: plain text object ID) |
Output — default (plain text)
sha256:a3f2...c8d1
Output — --json
{"object_id": "sha256:a3f2...c8d1", "stored": false, "size_bytes": 5}
stored is true only when --write is passed and the object was not
already in the store.
Exit codes: 0 success · 1 path not found or is a directory · 3 I/O write error or integrity check failed
cat-object — read a stored object
muse cat-object <object-id> [-j] [--inline]
muse cat-object --batch | --batch-check
Reads a content-addressed object from .muse/objects/. By default the raw
bytes are streamed to stdout at 64 KiB at a time — pipe to a file, another
process, or a network socket without any size ceiling. With --json a JSON
metadata summary is printed instead of the content.
Object IDs must be given in the canonical sha256:<64-hex> form — bare hex
is rejected.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit JSON metadata instead of raw bytes |
--inline |
— | off | With --json, embed the object content as base64 in content_b64 (requires --json) |
--batch |
— | off | Batch mode: read object IDs from stdin, one per line; emit <oid> blob <size>\n<content>\n per object (<oid> missing\n for absent/invalid ones) |
--batch-check |
— | off | Like --batch but header-only — no content bytes |
Output — default (raw bytes)
hello
Output — --json
{"object_id": "sha256:a3f2...c8d1", "present": true, "size_bytes": 4096}
With --inline:
{"object_id": "sha256:a3f2...c8d1", "present": true, "size_bytes": 5,
"content_b64": "aGVsbG8="}
When the object is absent and --json is used, present is false and
size_bytes is 0 (exit 1). When raw mode is used and the object is
absent, the error goes to stderr (exit 1).
Exit codes: 0 found · 1 not found or invalid ID format · 3 I/O read error. Batch mode (--batch/--batch-check) always exits 0 — missing objects are reported inline, not as failures.
rev-parse — resolve a ref to a commit ID
muse rev-parse <ref> [--abbrev-ref] [-j]
Resolves a branch name, HEAD, or an abbreviated SHA prefix to the full
64-character commit ID. Use this to canonicalise any ref before passing it to
other commands.
Arguments
| Argument | Description |
|---|---|
<ref> |
Branch name, HEAD, full commit ID, or unique prefix |
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--abbrev-ref |
— | off | Resolve to the branch name instead of the commit ID |
--json |
-j |
off | Emit machine-readable JSON (default: plain text commit ID) |
Output — default (plain text)
sha256:a3f2...c8d1
Output — --json
{"ref": "main", "commit_id": "sha256:a3f2...c8d1"}
Output — --abbrev-ref
{"ref": "HEAD", "branch": "main"}
Text mode with --abbrev-ref prints just main.
Ambiguous or unresolvable prefixes return a JSON error object on stdout
(exit 1): {"ref": "...", "commit_id": null, "error": "not found"}.
Exit codes: 0 resolved · 1 not found, ambiguous, or empty ref
ls-files — list files in a snapshot
muse ls-files [--commit <id>] [--path-prefix <prefix>] [-j]
Lists every file tracked in a commit's snapshot together with its content object ID. Defaults to the HEAD commit of the current branch.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--commit |
-c |
HEAD | Commit ID to inspect |
--path-prefix |
-p |
— | Only list files whose path starts with this prefix |
--json |
-j |
off | Emit machine-readable JSON (default: tab-separated text) |
Output — default (tab-separated, suitable for awk / cut)
sha256:c1d2...a3b4 tracks/bass.mid
sha256:e5f6...b7c8 tracks/drums.mid
sha256:09ab...cd10 tracks/piano.mid
Output — --json
{
"status": "ok",
"error": "",
"commit_id": "sha256:a3f2...c8d1",
"snapshot_id": "sha256:b7e4...f912",
"branch": "main",
"path_prefix": null,
"file_count": 3,
"files": [
{"path": "tracks/bass.mid", "object_id": "sha256:c1d2...a3b4"},
{"path": "tracks/drums.mid", "object_id": "sha256:e5f6...b7c8"},
{"path": "tracks/piano.mid", "object_id": "sha256:09ab...cd10"}
]
}
All keys are always present so agents can read them without dict.get
guards. "branch" is null when --commit is given explicitly (no branch
resolution occurs). "path_prefix" is null when no --path-prefix filter
was applied. Files are sorted by path.
On error, --json mode emits {"status": "error", "error": "...", "exit_code": 1} to stdout.
Exit codes: 0 listed · 1 commit or snapshot not found, or invalid argument
read-commit — print full commit metadata
muse read-commit <commit-id> [--fields FIELD,...] [-j]
Emits the complete JSON record for a commit. Accepts a full 64-character ID
or a unique prefix. Default output (no --json) is a compact one-line
summary; the full structured record requires --json.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--fields |
— | (all) | Comma-separated list of fields to include in JSON output — reduces response size for agent pipelines |
--json |
-j |
off | Emit machine-readable JSON (default: compact one-line text) |
Output — default (plain text)
sha256:a3f2...c8d1 main gabriel 2026-03-21T12:00:00+00:00 Add verse melody
Output — --json
{
"commit_id": "sha256:a3f2...c8d1",
"branch": "main",
"snapshot_id": "sha256:b7e4...f912",
"message": "Add verse melody",
"committed_at": "2026-03-18T12:00:00+00:00",
"parent_commit_id": "sha256:ff01...23ab",
"parent2_commit_id": null,
"author": "gabriel",
"agent_id": "",
"model_id": "",
"toolchain_id": "",
"sem_ver_bump": "none",
"breaking_changes": [],
"reviewed_by": [],
"test_runs": 0
}
Use --fields to trim the response, e.g.
muse read-commit <id> --fields commit_id,branch,message,committed_at --json.
Error conditions always produce JSON on stdout so scripts can parse them
without inspecting stderr.
Exit codes: 0 found · 1 not found, ambiguous prefix, or invalid ID format
read-snapshot — print full snapshot metadata
muse read-snapshot <snapshot-id> [--no-manifest] [--path-prefix <prefix>] [-j]
Emits the complete JSON record for a snapshot. Every commit references exactly
one snapshot. Use ls-files --commit <id> if you want to look up a snapshot
from a commit ID rather than the snapshot ID directly.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--no-manifest |
— | off | Omit the file manifest, returning only snapshot_id, created_at, file_count. Only valid with --json |
--path-prefix |
-p |
— | Filter the manifest to paths starting with this prefix |
--json |
-j |
off | Emit machine-readable JSON (default: compact one-line text) |
Output — default (plain text)
sha256:b7e4...f912 3 files 2026-03-21T12:00:00+00:00
Output — --json
{
"snapshot_id": "sha256:b7e4...f912",
"created_at": "2026-03-18T12:00:00+00:00",
"file_count": 3,
"manifest": {
"tracks/bass.mid": "sha256:c1d2...a3b4",
"tracks/drums.mid": "sha256:e5f6...b7c8",
"tracks/piano.mid": "sha256:09ab...cd10"
}
}
With --no-manifest --json:
{"snapshot_id": "sha256:b7e4...f912", "created_at": "2026-03-18T12:00:00+00:00", "file_count": 3}
Exit codes: 0 found · 1 not found, invalid ID format, or --no-manifest used without --json
commit-tree — create a commit from a snapshot ID
muse commit-tree -s <snapshot-id> [-p <parent-id>]... [-m <message>] [-a <author>] [-b <branch>] [--agent-id <id>] [--model-id <id>] [--toolchain-id <id>] [-j]
Low-level commit creation. The snapshot must already exist in the store. Both
the snapshot ID and any parent IDs are validated as proper 64-character SHA-256
hex strings before any I/O is attempted. Use --parent / -p once for a
linear commit and twice for a merge commit (at most two parents are
supported). The commit is written to .muse/commits/ but no branch ref is
updated — use update-ref to advance a branch to the new commit.
Flags
| Flag | Short | Required | Description |
|---|---|---|---|
--snapshot |
-s |
✅ | SHA-256 snapshot ID |
--parent |
-p |
— | Parent commit ID (repeat once for merges) |
--message |
-m |
— | Commit message |
--author |
-a |
— | Author name |
--branch |
-b |
— | Branch name to record (default: current branch) |
--agent-id |
— | — | Stable agent identifier (e.g. counterpoint-bot) |
--model-id |
— | — | Model identifier (e.g. claude-opus-4); empty for human authors |
--toolchain-id |
— | — | Toolchain that produced this commit (e.g. cursor-agent-v2) |
--json |
-j |
— | Emit machine-readable JSON (default: bare commit ID as text) |
Output — default (plain text)
sha256:a3f2...c8d1
Output — --json
{
"commit_id": "sha256:a3f2...c8d1",
"snapshot_id": "sha256:b7e4...f912",
"branch": "main",
"message": "feat: add melody",
"committed_at": "2026-03-18T12:00:00+00:00",
"author": "gabriel",
"agent_id": "counterpoint-bot",
"model_id": "claude-opus-4",
"toolchain_id": "cursor-agent-v2",
"parent_commit_id": "sha256:ff01...23ab",
"parent2_commit_id": null
}
The text form is ideal for shell pipelines where you want to capture the ID
directly without a jq call: NEW=$(muse commit-tree -s "$SNAP" -m "msg").
Agents should always pass --agent-id / --model-id (and --toolchain-id
where applicable) so their identity is auditable in the resulting commit.
Exit codes: 0 commit written · 1 snapshot or parent not found, invalid ID format, or repo.json unreadable · 3 write failure
update-ref — move a branch to a commit
muse update-ref <branch> <commit-id> [--no-verify] [--old-value <commit-id>] [-j]
muse update-ref <branch> --delete [-j]
Directly writes (or deletes) a branch reference file under .muse/refs/heads/.
The branch name is validated with the same rules as check-ref-format before
any file is written — path-traversal via crafted branch names is not possible.
The commit ID format is always validated regardless of --no-verify, so a
malformed ID can never corrupt the ref file.
By default, the commit must already exist in the local store (--verify is on);
pass --no-verify to write the ref before the commit is stored — useful after
an unpack-objects pipeline where objects arrive in dependency order.
Use --old-value for compare-and-swap semantics in multi-agent environments:
the update succeeds only if the current ref value matches the expected value.
Pass --old-value null to require that the ref does not currently exist.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--delete |
-d |
off | Delete the branch ref instead of updating it |
--no-verify |
— | off (verify is on) | Skip verifying the commit exists in the store before updating |
--old-value |
— | — | Compare-and-swap guard: only update if the current ref matches this commit ID (null = ref must not exist) |
--json |
-j |
off | Emit machine-readable JSON (default: silent on success) |
Output — default, update or delete (silent on success)
Nothing is printed; the command exits 0. This mirrors git update-ref,
making it drop-in compatible with shell scripts that use exit code only.
Output — --json, update
{"branch": "main", "commit_id": "sha256:a3f2...c8d1", "previous": "sha256:ff01...23ab"}
previous is null when the branch had no prior commit.
Output — --json, delete
{"branch": "feat/x", "deleted": true}
CAS failures include the conflicting values:
{"error": "cas_mismatch", "message": "CAS mismatch: ...",
"current": "sha256:...", "expected": "sha256:..."}
Exit codes: 0 done · 1 commit not in store (with verify on), invalid branch or commit ID, --delete on non-existent ref, or CAS mismatch · 3 file write failure
commit-graph — emit the commit DAG
muse commit-graph [--tip <id>] [--stop-at <id>] [-n <max>] [-c] [-1] [-a] [-j]
Performs a BFS walk from a tip commit (defaulting to HEAD), following both
parent_commit_id and parent2_commit_id pointers. Returns every reachable
commit. Useful for building visualisations, computing reachability sets, or
finding the commits on a branch since it diverged from another.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--tip |
— | HEAD | Commit to start from |
--stop-at |
— | — | Stop BFS at this commit (exclusive) |
--max |
-n |
10 000 | Maximum commits to traverse |
--count |
-c |
off | Emit only the integer count, not the full node list |
--first-parent |
-1 |
off | Follow only first-parent links — linear history, no merge parents |
--ancestry-path |
-a |
off | With --stop-at: restrict to commits on the direct path between tip and stop-at |
--json |
-j |
off | Emit machine-readable JSON (default: one commit ID per line, prefixed with a # TRUNCATED comment line if --max cut the walk short) |
Output — default (plain text)
sha256:a3f2...c8d1
sha256:ff01...23ab
Output — --json
{
"tip": "sha256:a3f2...c8d1",
"count": 42,
"truncated": false,
"commits": [
{
"commit_id": "sha256:a3f2...c8d1",
"parent_commit_id": "sha256:ff01...23ab",
"parent2_commit_id": null,
"message": "Add verse melody",
"branch": "main",
"committed_at": "2026-03-18T12:00:00+00:00",
"snapshot_id": "sha256:b7e4...f912",
"author": "gabriel",
"agent_id": "claude-code",
"model_id": "claude-sonnet-4-6",
"sem_ver_bump": "minor",
"breaking_changes": []
}
]
}
truncated is true when the graph was cut off by --max.
Output — --count --json
{"tip": "sha256:a3f2...c8d1", "count": 42, "truncated": false}
--count suppresses the commits array entirely, making it suitable for fast
cardinality checks without loading commit metadata.
Examples
Commits on a feature branch since it diverged from main:
BASE=$(muse merge-base feat/x main)
muse commit-graph --tip feat/x --stop-at "$BASE"
Count commits in a feature branch:
muse commit-graph \
--tip "$(muse rev-parse feat/x)" \
--stop-at "$(muse merge-base feat/x dev)" \
--count --json
Linear history only (skip merge parents):
muse commit-graph --first-parent
Exit codes: 0 graph emitted · 1 tip commit not found, or --ancestry-path without --stop-at
pack-objects — pack commits for transport
muse pack-objects <want>... [--have <id>...] [--dry-run] [-j]
Collects a set of commits — and all their referenced snapshots and objects —
into a single MPack (msgpack binary) written to stdout. Pass --have to
tell the packer which commits the receiver already has; objects reachable
only from --have ancestors are excluded, minimising transfer size.
<want> may be a full commit ID or HEAD.
Flags
| Flag | Short | Description |
|---|---|---|
--have |
— | Commits the receiver already has (repeat for multiple) |
--dry-run |
-n |
Print a JSON summary of what would be packed instead of writing binary msgpack |
--json |
-j |
Shorthand equivalent used with --dry-run to emit the JSON summary |
Output — default — an MPack msgpack binary (pipe to a file or unpack-objects)
Output — --dry-run
{"want": ["sha256:a3f2...c8d1"], "have": [], "commits": 3, "snapshots": 3, "blobs": 12, "object_bytes": 40960}
object_bytes is the total uncompressed byte size of all object payloads in
the pack — use it to decide whether to buffer the full mpack in memory or
stream it directly to the remote.
Exit codes: 0 pack written (or dry-run summary printed) · 1 a wanted commit not found or HEAD has no commits · 3 I/O error reading from the local store
unpack-objects — apply an mpack to the local store
cat pack.muse | muse unpack-objects [-j]
muse pack-objects HEAD | muse unpack-objects
Reads an MPack from stdin and writes its commits, snapshots, blobs, and
tags into .muse/. Idempotent: objects already present in the store are
silently skipped. Partial packs from interrupted transfers are safe to
re-apply.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: human-readable summary) |
Output — default (plain text)
Wrote 12 commits, 12 snapshots, 47 blobs (3 skipped), 2 tags.
Output — --json
{
"commits_written": 12,
"snapshots_written": 12,
"blobs_written": 47,
"blobs_skipped": 3,
"tags_written": 2
}
Exit codes: 0 unpacked (all objects stored) · 1 invalid or malformed msgpack input, or input is not a top-level map · 3 write failure
ls-remote — list refs on a remote
muse ls-remote [<remote-or-url>] [-j]
Contacts a remote and lists every branch HEAD without altering local state.
The <remote-or-url> argument is either a remote name configured with
muse remote add (defaults to origin) or a full https:// URL.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: tab-separated text) |
Output — default (plain text)
One line per branch, tab-separated. The default branch is marked with *.
sha256:a3f2...c8d1 main *
sha256:b7e4...f912 feat/experiment
Output — --json
{
"status": "ok",
"error": "",
"repo_id": "sha256:550e8400...",
"domain": "midi",
"default_branch": "main",
"branches": {
"main": "sha256:a3f2...c8d1",
"feat/experiment": "sha256:b7e4...f912"
},
"remote": "origin",
"url": "https://musehub.ai/org/repo"
}
All keys are always present so agents can read them without dict.get
guards. "remote" is null when the caller passed a full URL directly
instead of a configured remote name. Branch commit IDs are always
sha256:-prefixed regardless of what the remote returns.
Exit codes: 0 remote contacted · 1 remote not configured or URL invalid · 3 transport error (network, HTTP error)
Composability Patterns
Export a history range
# All commits on feat/x that are not on main
BASE=$(muse rev-parse main)
TIP=$(muse rev-parse feat/x)
muse commit-graph --tip "$TIP" --stop-at "$BASE"
Ship commits between two machines
# On the sender — pack everything the receiver doesn't have
HAVE=$(muse ls-remote origin | awk '{print "--have " $1}' | tr '\n' ' ')
muse pack-objects HEAD $HAVE > bundle.muse
# On the receiver
cat bundle.muse | muse unpack-objects
muse update-ref main <commit-id>
Verify a stored object
ID=$(muse hash-object tracks/drums.mid)
muse cat-object "$ID" --json
Inspect what changed in the last commit
muse read-commit "$(muse rev-parse HEAD)" --json | \
python3 -c "import sys, json; d=json.load(sys.stdin); print(d['message'])"
Script a bare commit (advanced)
# 1. Hash and store the files
OID=$(muse hash-object -w tracks/drums.mid)
# 2. Build a snapshot manifest and write it (via muse commit is easier,
# but for full control use commit-tree after writing the snapshot)
SNAP=$(muse rev-parse HEAD | \
xargs -I{} muse read-commit {} --json | \
python3 -c "import sys,json; print(json.load(sys.stdin)['snapshot_id'])")
# 3. Create a commit on top of HEAD
PARENT=$(muse rev-parse HEAD)
NEW=$(muse commit-tree -s "$SNAP" -p "$PARENT" -m "scripted commit" --json | \
python3 -c "import sys,json; print(json.load(sys.stdin)['commit_id'])")
# 4. Advance the branch
muse update-ref main "$NEW"
merge-base — find the common ancestor of two commits
Find the lowest common ancestor of two commits — the point at which two branches diverged.
muse merge-base <commit-a> <commit-b> [-j]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: plain text commit ID) |
Arguments accept full SHA-256 commit IDs, branch names, or HEAD.
Default output (plain text):
sha256:<sha256>
--json output:
{
"commit_a": "sha256:<sha256>",
"commit_b": "sha256:<sha256>",
"merge_base": "sha256:<sha256>"
}
When no common ancestor exists, merge_base is null and error is set.
| Exit | Meaning |
|---|---|
| 0 | Result computed (check merge_base for null vs. found) |
| 1 | A commit ID or ref cannot be resolved |
| 3 | DAG walk failed |
snapshot-diff — diff two snapshot manifests
Compare two snapshots and categorise every changed path as added, modified, or deleted.
muse snapshot-diff <ref-a> <ref-b> [-j] [-s] [--raw] [--only CATEGORY] [--path-prefix PREFIX]
muse snapshot-diff --stdin [-j]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit JSON output (default: human-readable text) |
--stat |
-s |
off | Append a summary line in text mode |
--raw |
— | off | Include object IDs in text-format output (no effect on JSON, which always includes them) |
--only |
— | — | Restrict output to one category: added, modified, or deleted |
--path-prefix |
— | — | Filter the diff to paths starting with this prefix |
--stdin |
— | off | Batch mode: read <ref-a> <ref-b> pairs from stdin, one per line |
Arguments accept snapshot IDs, commit IDs, branch names, or HEAD.
Default output (text):
A new.mid
M main.mid
D old.mid
With --raw:
A sha256:<oid_b> new.mid
M sha256:<oid_a> sha256:<oid_b> main.mid
D sha256:<oid_a> old.mid
--json output:
{
"snapshot_a": "sha256:<sha256>",
"snapshot_b": "sha256:<sha256>",
"added": [{"path": "new.mid", "object_id": "sha256:<sha256>"}],
"modified": [{"path": "main.mid", "object_id_a": "sha256:<sha256>", "object_id_b": "sha256:<sha256>"}],
"deleted": [{"path": "old.mid", "object_id": "sha256:<sha256>"}],
"added_count": 1,
"modified_count": 1,
"deleted_count": 1,
"total_changes": 3
}
--only restricts which category is populated (the other two are emitted as
empty arrays in JSON mode, or simply not printed in text mode). --path-prefix
is applied before --only, and counts/total_changes reflect the filtered
view.
| Exit | Meaning |
|---|---|
| 0 | Diff computed (zero changes is a valid result) |
| 1 | Ref cannot be resolved, or bad --only value |
| 3 | I/O error reading snapshot records |
Batch mode (--stdin) always exits 0; individual pair errors are reported inline.
domain-info — inspect the active domain plugin
Inspect the domain plugin active for this repository — its name, class, optional protocol capabilities, and full structural schema.
muse domain-info [-j] [-a] [--domain DOMAIN] [--capabilities-only]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
--all-domains |
-a |
off | List every registered domain; no repo required |
--domain |
— | — | Inspect a specific domain by name without requiring an active repo |
--capabilities-only |
— | off | Emit only the capabilities dict |
Default output (text):
Domain: midi
Plugin: MidiPlugin
Merge mode: three_way
Capabilities: addressed_merge
--json output:
{
"domain": "midi",
"plugin_class": "MidiPlugin",
"capabilities": {
"addressed_merge": true,
"crdt": false,
"harmony": false
},
"schema": {
"domain": "midi", "merge_mode": "three_way",
"dimensions": [...], "top_level": {...}
},
"registered_domains": ["bitcoin", "code", "midi", "scaffold"]
}
| Exit | Meaning |
|---|---|
| 0 | Domain resolved and schema emitted |
| 1 | No repository found, or domain not registered |
| 3 | Plugin raised an error computing its schema |
show-ref — list all branch refs
List all branch refs and the commit IDs they point to.
muse show-ref [-j] [-p PATTERN] [-H] [-v REF] [--count]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
--pattern |
-p |
"" |
fnmatch glob to filter ref names |
--head |
-H |
off | Print only HEAD ref and commit ID |
--verify |
-v |
"" |
Silent existence check — exit 0 if found, 1 if not |
--count |
— | off | Emit only the branch count |
Default output (text):
sha256:<sha256> refs/heads/dev
* sha256:<sha256> refs/heads/main (HEAD)
--json output:
{
"refs": [
{"ref": "refs/heads/dev", "commit_id": "sha256:<sha256>"},
{"ref": "refs/heads/main", "commit_id": "sha256:<sha256>"}
],
"head": {"ref": "refs/heads/main", "branch": "main", "commit_id": "sha256:<sha256>"},
"count": 2
}
Use --verify in shell conditionals:
muse show-ref --verify refs/heads/my-branch && echo "branch exists"
| Exit | Meaning |
|---|---|
| 0 | Refs enumerated (or --verify ref exists) |
| 1 | --verify ref absent |
| 3 | I/O error reading refs directory |
check-ignore — test whether paths are excluded by .museignore
Test whether workspace paths are excluded by .museignore rules.
muse check-ignore <path>... [-j] [-q] [-V] [--stdin] [--patterns-only] [--ignored-only]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
--quiet |
-q |
off | No output; exit 0 if all ignored, 1 otherwise |
--verbose |
-V |
off | Include matching pattern in text output (JSON always includes it) |
--stdin |
— | off | Read additional paths from stdin, one per line |
--patterns-only |
— | off | Emit the resolved pattern list and exit; no path arguments needed |
--ignored-only |
— | off | Show only paths that are ignored |
Default output (text):
ignored build/output.bin [build/]
ok tracks/dr.mid
--json output:
{
"domain": "midi",
"patterns_loaded": 4,
"summary": {"total": 2, "ignored": 1, "not_ignored": 1},
"results": [
{"path": "build/out.bin", "ignored": true, "matching_pattern": "build/"},
{"path": "tracks/dr.mid", "ignored": false, "matching_pattern": null}
]
}
With --patterns-only:
{"domain": "midi", "patterns_loaded": 3, "patterns": ["build/", "*.bin", "!tracks/*.mid"]}
Last-match-wins: a negation rule (!important.mid) can un-ignore a path
matched by an earlier rule.
| Exit | Meaning |
|---|---|
| 0 | Results emitted (or --quiet with all ignored) |
| 1 | --quiet with any non-ignored path; missing args; --ignored-only combined with --patterns-only or --quiet |
| 3 | I/O or TOML parse error reading .museignore |
check-attr — query merge-strategy attributes for paths
Query merge-strategy attributes for workspace paths from .museattributes.
muse check-attr <path>... [-j] [-d DIMENSION] [-A] [--stdin] [--rules-only] [--unmatched-only]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
--dimension |
-d |
* |
Domain axis to query (e.g. notes, tempo) |
--all-rules |
-A |
off | Return every matching rule, not just first-match |
--stdin |
— | off | Read additional paths from stdin, one per line |
--rules-only |
— | off | Emit the loaded rule list without testing any paths |
--unmatched-only |
— | off | Show only paths with no matching rule (strategy=auto) |
--json output (default: first-match):
{
"domain": "midi",
"rules_loaded": 3,
"dimension": "*",
"summary": {"total": 2, "matched": 1, "unmatched": 1, "by_strategy": {"ours": 1, "auto": 1}},
"results": [
{
"path": "drums/kit.mid",
"dimension": "*",
"strategy": "ours",
"rule": {"path_pattern": "drums/*", "strategy": "ours", "priority": 10, ...}
}
]
}
When no rule matches, strategy is "auto" and rule is null.
| Exit | Meaning |
|---|---|
| 0 | Attributes resolved and emitted |
| 1 | Missing args; --unmatched-only combined with --rules-only |
| 3 | TOML parse error in .museattributes |
verify-object — re-hash stored objects to detect corruption
Re-hash stored objects to detect silent data corruption.
muse verify-object <object-id>... [-j] [-q] [--all] [--stdin] [--fail-fast]
| Flag | Short | Default | Description |
|---|---|---|---|
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
--quiet |
-q |
off | No output; exit 0 if all OK, 1 otherwise |
--all |
-a |
off | Verify every object in the store — the fsck equivalent |
--stdin |
— | off | Read additional object IDs from stdin, one per line |
--fail-fast |
— | off | Stop after the first failed object and exit 1 immediately |
Objects are streamed in 64 KiB chunks — safe for very large blobs.
Default output (text):
OK sha256:<sha256> (4096 bytes)
FAIL sha256:<sha256> object not found in store
---
Checked: 2 Failed: 1
--json output:
{
"results": [
{"object_id": "sha256:<sha256>", "ok": true, "size_bytes": 4096, "error": null},
{"object_id": "sha256:<sha256>", "ok": false, "size_bytes": null,
"error": "object not found in store"}
],
"all_ok": false,
"checked": 2,
"failed": 1
}
Compose with show-ref to verify every commit in a repo:
muse show-ref --json \
| jq -r '.refs[].commit_id' \
| xargs muse verify-object
| Exit | Meaning |
|---|---|
| 0 | All objects verified successfully |
| 1 | One or more objects failed; object not found; bad args |
| 3 | Unexpected I/O error (disk read failure) |
symbolic-ref — read or write HEAD's symbolic reference
In Muse, HEAD is normally a symbolic reference — it points to a branch — but
can also be detached, pointing directly at a commit. symbolic-ref reads
which branch HEAD tracks or, with --set, points HEAD at a different branch.
# Read mode
muse symbolic-ref HEAD [-j] [--short]
# Write mode
muse symbolic-ref HEAD --set <branch> [--create-branch] [-j]
| Flag | Short | Default | Description |
|---|---|---|---|
--set |
-s |
"" |
Branch name to point HEAD at |
--create-branch |
— | off | With --set, create the branch pointer even if it has no commits yet |
--short |
-S |
off | Emit branch name only (not the full refs/heads/… path) |
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
Default output (text, read mode):
refs/heads/main
With --short: main.
--json output (read mode, normal branch HEAD):
{
"ref": "HEAD",
"symbolic_target": "refs/heads/main",
"branch": "main",
"commit_id": "sha256:<sha256>",
"detached": false
}
--json output (read mode, detached HEAD):
{
"ref": "HEAD",
"symbolic_target": null,
"branch": null,
"commit_id": "sha256:<sha256>",
"detached": true
}
commit_id is null when the branch has no commits yet. --set without
--create-branch requires the target branch to already exist.
| Exit | Meaning |
|---|---|
| 0 | Ref read or written |
| 1 | --set target branch does not exist (without --create-branch); unsupported ref name; invalid branch name |
| 3 | I/O error reading or writing HEAD |
for-each-ref — iterate all refs with rich commit metadata
Enumerates every branch ref together with the full commit metadata it points to. Supports sorting by any commit field and glob-pattern filtering, making it ideal for agent pipelines that need to slice the ref list without post-processing.
muse for-each-ref [-p <pattern>] [-s <field>] [-d] [--count <count>] [--no-commits] [-j]
| Flag | Short | Default | Description |
|---|---|---|---|
--pattern |
-p |
"" |
fnmatch glob on the full ref name, e.g. refs/heads/feat/* |
--sort |
-s |
ref |
Sort field: ref, branch, commit_id, author, committed_at, message, snapshot_id |
--desc |
— | off | Reverse sort order (descending) |
--count |
— | 0 |
Limit to first N refs after sorting (0 = unlimited) |
--no-commits |
— | off | Skip loading commit records; emit only ref, branch, commit_id |
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
Default output (text): <commit_id> <ref> <committed_at> <author> (or just <commit_id> <ref> with --no-commits)
--json output:
{
"refs": [
{
"ref": "refs/heads/dev",
"branch": "dev",
"commit_id": "sha256:<sha256>",
"author": "gabriel",
"message": "Add verse melody",
"committed_at": "2026-01-01T00:00:00+00:00",
"snapshot_id": "sha256:<sha256>"
}
],
"count": 1,
"current_branch": "dev"
}
With --no-commits, author / message / committed_at / snapshot_id are omitted.
Example — three most recently committed branches:
muse for-each-ref --sort committed_at --desc --count 3
| Exit | Meaning |
|---|---|
| 0 | Refs emitted (list may be empty) |
| 1 | Unknown --sort field, or negative --count |
| 3 | I/O error reading refs or commit records |
name-rev — map commit IDs to branch-relative names
For each supplied commit ID, performs a single multi-source BFS from all branch
tips and reports the closest branch and hop distance. Results are expressed as
<branch>~N — where N is the number of parent hops from the tip. When N is 0
(the commit is the exact branch tip) the name is the bare branch name with no
~0 suffix.
muse name-rev <commit-id>... [-j] [--name-only] [-u <string>] [--stdin] [--branches GLOB] [--max-walk N]
| Flag | Short | Default | Description |
|---|---|---|---|
--name-only |
— | off | Emit only the name (or the undefined string), not the commit ID |
--undefined |
-u |
"undefined" |
String to emit for unreachable commits |
--stdin |
— | off | Read additional commit IDs from stdin, one per line |
--branches |
— | — | Restrict BFS seeds to branch names matching this fnmatch glob |
--max-walk |
— | 50 000 | Maximum BFS steps before stopping |
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
Default output (text): <sha256> main~3 (or main~3 with --name-only)
--json output:
{
"results": [
{
"commit_id": "sha256:<sha256>",
"input": "<sha256>",
"name": "main~3",
"branch": "main",
"distance": 3,
"undefined": false,
"ambiguous": false
},
{
"commit_id": null,
"input": "deadbeef",
"name": null,
"branch": null,
"distance": null,
"undefined": true,
"ambiguous": false
}
]
}
Performance: A single O(total-commits) BFS from all branch tips simultaneously. Every commit is visited at most once regardless of how many input IDs are supplied.
| Exit | Meaning |
|---|---|
| 0 | All results computed (some may be undefined or ambiguous) |
| 1 | No commit IDs supplied, or non-hex input rejected |
| 3 | I/O error reading commit records |
check-ref-format — validate branch and ref names
Tests one or more names against Muse's branch-naming rules — the same
validation used by muse branch and muse update-ref. Use in
scripts to pre-validate names before attempting to create a branch.
muse check-ref-format <name>... [-q] [-j] [--stdin] [--rules] [--invalid-only]
| Flag | Short | Default | Description |
|---|---|---|---|
--stdin |
— | off | Read additional names from stdin, one per line |
--rules |
— | off | Emit the validation ruleset as JSON and exit |
--quiet |
-q |
off | No output — exit 0 if all valid, exit 1 otherwise |
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
--invalid-only |
— | off | Show only invalid names in results |
Rules enforced: 1–255 chars; no C0 control characters, space, or DEL; no
backslash; no Git-banned punctuation (~ ^ : ? * [); no leading/trailing dot;
no consecutive dots (..); no leading/trailing or consecutive slashes; no
single-dot path component; no component ending in .lock; no @{ sequence;
not the bare string @.
--json output:
{
"results": [
{"name": "feat/my-branch", "valid": true, "error": null},
{"name": "bad..name", "valid": false, "error": "..."}
],
"all_valid": false,
"valid_count": 1,
"invalid_count": 1
}
Default output (text):
ok feat/my-branch
FAIL bad..name → Branch name 'bad..name' contains forbidden characters
With --rules:
{"max_length": 255, "forbidden_chars": [...], "forbidden_patterns": [...], "notes": "..."}
Shell conditional:
muse check-ref-format -q "$BRANCH" && muse checkout -b "$BRANCH"
| Exit | Meaning |
|---|---|
| 0 | All names are valid, or --rules was used |
| 1 | One or more names are invalid; no names supplied; --invalid-only combined with --quiet or --rules |
(No exit 3 — this command is pure CPU, no I/O.)
verify-pack — verify MPack integrity
Reads an MPack from stdin or --file and performs three-tier integrity
checking:
- Blob integrity — every blob payload's SHA-256 is recomputed from the
raw bytes. The digest must match the declared
object_id. - Snapshot consistency — every snapshot's manifest entries reference objects present in the bundle or already in the local store.
- Commit consistency — every commit's
snapshot_idis present in the bundle or already in the local store.
muse pack-objects main | muse verify-pack
muse verify-pack --file bundle.muse
| Flag | Short | Default | Description |
|---|---|---|---|
--file |
-i |
"" |
Path to bundle file (reads stdin when omitted) |
--stat |
— | off | Fast structural inspection — counts only, no hashing |
--quiet |
-q |
off | No output — exit 0 if clean, exit 1 on any failure |
--no-local |
-L |
off | Skip local store checks (verify bundle in isolation) |
--strict |
— | off | Treat promised objects (absent locally but covered by a promisor remote) as integrity failures |
--json |
-j |
off | Emit machine-readable JSON (default: human-readable text) |
Default output (text):
blobs=42 snapshots=5 commits=5 all_ok=True
--json output:
{
"blobs_checked": 42,
"snapshots_checked": 5,
"commits_checked": 5,
"all_ok": true,
"failures": [],
"promised_objects": 0,
"base_objects": 0,
"bundle_mode": "full",
"base_commits": []
}
With failures:
{
"all_ok": false,
"failures": [
{"kind": "object", "id": "sha256:<sha256>", "error": "hash mismatch"},
{"kind": "snapshot", "id": "sha256:<sha256>", "error": "missing object: ..."}
]
}
Objects absent from the mpack are resolved against the local store using a
three-state model: PRESENT (verified, not a failure), PROMISED (absent
locally but a promisor remote is configured — counted separately, not a
failure unless --strict), MISSING (absent with no promisor — always a
failure).
Validate before upload:
muse pack-objects main | muse verify-pack -q \
&& echo "mpack is clean — safe to push"
| Exit | Meaning |
|---|---|
| 0 | MPack is fully intact (or --stat completed) |
| 1 | One or more integrity failures; malformed msgpack; bad args |
| 3 | I/O error reading stdin or the mpack file |
Composability Patterns — Advanced
Name every commit reachable from a branch
# Get all commit IDs on feat/x since it diverged from dev
BASE=$(muse merge-base feat/x dev)
muse commit-graph --tip feat/x --stop-at "$BASE" \
| xargs muse name-rev --name-only
Audit all refs with full metadata and filter by recency
# List all branches modified in 2026, sorted newest-first
muse for-each-ref --sort committed_at --desc --json \
| jq '.refs[] | select(.committed_at | startswith("2026"))'
Validate a branch name before creating it
BRANCH="feat/my-feature"
muse check-ref-format -q "$BRANCH" \
&& echo "Name is valid — safe to branch" \
|| echo "Invalid branch name"
Verify a bundle before shipping
muse pack-objects main | tee bundle.muse | muse verify-pack -q \
&& echo "bundle is clean — safe to push" \
|| echo "bundle has integrity failures — do not push"
Switch active branch via plumbing
# Check where HEAD is now
muse symbolic-ref HEAD # → refs/heads/main
# Redirect HEAD to dev
muse symbolic-ref HEAD --set dev
muse rev-parse HEAD # → tip of dev
Find stale branches (no commits in the last 30 days)
# Requires `date` and `jq`
CUTOFF=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|| date -u --date="30 days ago" +%Y-%m-%dT%H:%M:%SZ)
muse for-each-ref --json \
| jq --arg c "$CUTOFF" '.refs[] | select(.committed_at < $c) | .branch'
Check which files changed between two branches
BASE=$(muse merge-base main feat/x)
muse snapshot-diff "$BASE" feat/x --stat
Object ID Quick Reference
All IDs in Muse are canonical sha256:<64-char lowercase hex> digests. There are
three kinds:
| Kind | Computed from | Used by |
|---|---|---|
| Object ID | File bytes | hash-object, cat-object, snapshot manifests |
| Snapshot ID | Sorted path:object_id pairs |
read-snapshot, commit-tree |
| Commit ID | Parent IDs + snapshot ID + message + timestamp | read-commit, rev-parse, update-ref |
Every ID is deterministic and content-addressed. The same input always produces the same ID; two different inputs never produce the same ID in practice.
Exit Code Summary
| Code | Constant | Meaning |
|---|---|---|
| 0 | SUCCESS |
Command completed successfully |
| 1 | USER_ERROR |
Bad input, ref not found, invalid format |
| 3 | INTERNAL_ERROR |
I/O failure, integrity check, transport error |