muse tag — Protocol-Native Version Tags
The complete developer guide with real command output.
Muse version tags are a first-class VCS primitive: local-first, semver-native, and
push-aware. They are the lightweight equivalent of git tag v1.2.3 — minus the footguns.
This guide walks through every operation from scratch with actual command output so you
know exactly what to expect.
Why version tags?
Muse has two annotation systems that serve different purposes:
| Tool | Purpose | Hub required? |
|---|---|---|
muse label add emotion:joyful |
Freeform namespace:value annotations — musical metadata, emotional tags, section markers | No |
muse tag add v0.2.0-rc14 |
Protocol-native semantic version stamps — release markers, CI gates, describe reference points |
No |
muse release add v1.0.0 |
Full MuseHub release with changelog, asset attachments, draft flag | Yes |
Version tags live in .muse/version-tags/ as content-addressed JSON. They push and pull
alongside branches. They are the reference points for muse describe. No hub connection
needed to create them.
Tagging a commit
$ muse tag add v0.2.0-rc14 --json
{
"exit_code": 0,
"duration_ms": 0.6,
"muse_version": "0.2.0rc14",
"schema": 1,
"timestamp": "2026-06-28T22:17:43.201Z",
"warnings": [],
"status": "tagged",
"tag_id": "sha256:a3f2c901…",
"commit_id": "sha256:7d84ef12…",
"tag": "v0.2.0-rc14",
"semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" },
"author": "gabriel",
"created_at": "2026-06-28T22:17:43+00:00",
"message": "",
"dry_run": false
}
Tags HEAD by default. To tag any other commit or branch:
$ muse tag add v0.1.0 abc123def # by commit ID prefix
$ muse tag add v0.1.0 feat/audio # by branch name
Add an annotation message:
$ muse tag add v0.2.0 -m "stable: all E2E green, docs reviewed"
Validate before writing:
$ muse tag add v0.2.0-rc99 --dry-run --json
{
"status": "dry_run",
"tag_id": null,
"commit_id": "sha256:7d84ef12…",
"tag": "v0.2.0-rc99",
...
}
Nothing is written to disk. tag_id is null.
What "tagged" means on disk
Every tag is one JSON file in .muse/version-tags/:
.muse/version-tags/
v0.1.0.json
v0.2.0-rc13.json
v0.2.0-rc14.json
$ cat .muse/version-tags/v0.2.0-rc14.json
{
"tag_id": "sha256:a3f2c901…",
"repo_id": "sha256:5c8e2a0f…",
"tag": "v0.2.0-rc14",
"semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" },
"commit_id": "sha256:7d84ef12…",
"created_at": "2026-06-28T22:17:43+00:00",
"author": "gabriel",
"message": ""
}
Content-addressed: the tag_id is a deterministic SHA-256 of repo_id + ":" + tag.
Tamper-evident. Survives bundle transfer and offline workflows.
Idempotent tagging
Re-tagging the same commit at the same version is a no-op:
$ muse tag add v0.2.0-rc14 --json
{
"status": "already_tagged",
"tag_id": "sha256:a3f2c901…",
...
}
To move an existing tag to a new commit (e.g. after an amendment):
$ muse tag add v0.2.0-rc14 --force
Listing tags
$ muse tag list --json
{
"exit_code": 0,
"total": 2,
"tags": [
{
"tag": "v0.2.0-rc14",
"semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" },
"commit_id": "sha256:7d84ef12…",
"author": "gabriel",
"created_at": "2026-06-28T22:17:43+00:00",
"message": ""
},
{
"tag": "v0.1.0",
"semver": { "major": 0, "minor": 1, "patch": 0, "pre": "", "build": "" },
"commit_id": "sha256:b2a91c03…",
"author": "gabriel",
"created_at": "2026-06-01T18:00:00+00:00",
"message": "first stable release"
}
]
}
Without --pre, only stable tags are shown (pre: ""). This is intentional: your CI
pipeline should list stable releases by default; pass --pre when you need the full
picture including release candidates.
$ muse tag list --pre --json # includes v0.2.0-rc14, v0.1.0
$ muse tag list --json # only v0.1.0 (stable only)
Sort options:
$ muse tag list --sort created # newest created_at first
$ muse tag list --sort commit # lexicographic by commit_id
$ muse tag list --sort semver # semver precedence (default)
Semver precedence
Muse implements full SemVer 2.0 ordering. From highest to lowest:
v2.0.0 stable — always above any pre-release of the same M.M.P
v1.0.0
v0.2.0 stable — above v0.2.0-rc99
v0.2.0-rc14 rc beats beta; higher number beats lower
v0.2.0-rc13
v0.2.0-beta.2 beta beats alpha
v0.2.0-beta.1
v0.2.0-alpha.1
v0.1.0
Supported pre-release forms:
rc<N> v1.0.0-rc1, v1.0.0-rc14
alpha v1.0.0-alpha
alpha.<N> v1.0.0-alpha.1, v1.0.0-alpha.2
beta v1.0.0-beta
beta.<N> v1.0.0-beta.2
Build metadata is ignored in ordering per SemVer §10:
v1.0.0+build.42 == v1.0.0 (ordering only; metadata stored)
Reading and deleting a tag
$ muse tag read v0.2.0-rc14 --json
{
"exit_code": 0,
"tag": "v0.2.0-rc14",
"semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" },
"commit_id": "sha256:7d84ef12…",
"author": "gabriel",
"created_at": "2026-06-28T22:17:43+00:00",
"message": ""
}
Missing tag exits 1 with "error": "version_tag_not_found".
$ muse tag delete v0.2.0-rc13 --json
{
"exit_code": 0,
"deleted": true,
"tag": "v0.2.0-rc13"
}
$ muse tag delete v0.2.0-rc13 --json # idempotent
{
"exit_code": 0,
"deleted": false,
"tag": "v0.2.0-rc13"
}
Latest tag
$ muse tag latest --json
{
"exit_code": 0,
"latest": {
"tag": "v0.1.0",
"commit_id": "sha256:b2a91c03…",
...
}
}
$ muse tag latest --pre --json
{
"exit_code": 0,
"latest": {
"tag": "v0.2.0-rc14",
"commit_id": "sha256:7d84ef12…",
...
}
}
When no matching tags exist, "latest": null, exit 0.
Pushing a tag to a remote
$ muse tag push local v0.2.0-rc14 --json
{
"exit_code": 0,
"status": "pushed",
"remote": "local",
"tag": "v0.2.0-rc14",
"commit_id": "sha256:7d84ef12…"
}
Already current on the remote:
$ muse tag push local v0.2.0-rc14 --json
{
"exit_code": 0,
"status": "already_current",
"remote": "local",
"tag": "v0.2.0-rc14"
}
Conflict — remote tag points to a different commit:
$ muse tag push local v0.2.0-rc14 --json
{
"exit_code": 1,
"error": "remote_tag_conflict",
"message": "Remote tag v0.2.0-rc14 exists at a different commit. Use --force to overwrite."
}
$ muse tag push local v0.2.0-rc14 --force # overwrite
Pushing tags with a branch
The idiomatic workflow: push the branch and all tags in one command.
$ muse push local dev --tags --json
{
"exit_code": 0,
"commits_sent": 3,
"tags_pushed": 2,
"tags_skipped": 0,
...
}
tags_skipped counts tags already current on the remote.
To make --tags the default for every push in this repo:
$ muse config set push.tags true
$ muse config get push.tags --json
{ "key": "push.tags", "value": "true" }
$ muse push local dev # now includes tags automatically
Fetching tags from a remote
muse pull fetches version tags alongside commits:
$ muse pull local main
✔ 3 commits fetched
✔ 2 version tags fetched: v0.1.0, v0.2.0-rc14
After pulling, muse tag list --pre shows the fetched tags locally.
muse describe — position relative to the nearest tag
muse describe walks backward from HEAD and finds the nearest version tag.
It is the Muse equivalent of git describe --tags.
HEAD is exactly on a tag:
$ muse describe --json
{
"exit_code": 0,
"tag": "v0.1.0",
"distance": 0,
"short_sha": "b2a91c03",
"description": "v0.1.0",
"exact": true,
"commit_id": "sha256:b2a91c03…",
"branch": "main"
}
HEAD is 3 commits ahead of a tag:
$ muse describe --json
{
"exit_code": 0,
"tag": "v0.1.0",
"distance": 3,
"short_sha": "7d84ef12",
"description": "v0.1.0-3-7d84ef12",
"exact": false,
"commit_id": "sha256:7d84ef12…",
"branch": "dev"
}
The description string v0.1.0-3-7d84ef12 tells you: 3 commits past tag v0.1.0,
at the commit starting with 7d84ef12.
Without --pre, only stable tags are reference points:
# Only RC tags exist:
$ muse describe --json
{ "exit_code": 1, "tag": null, "distance": null, "description": null, ... }
$ muse describe --pre --json
{ "exit_code": 0, "tag": "v0.2.0-rc14", "distance": 0, "description": "v0.2.0-rc14", ... }
Describe from a specific branch or commit:
$ muse describe feat/audio --json
$ muse describe sha256:7d84ef12 --json
Control the hex suffix length:
$ muse describe --abbrev 12 --json
{
"short_sha": "7d84ef12a3b9",
"description": "v0.1.0-3-7d84ef12a3b9"
}
Default is 8. Range: 4–64. No sha256: prefix — raw hex only.
Full round-trip workflow
# Start new feature work
muse checkout dev
muse checkout -b task/audio-export --intent "implement audio export" --resumable
# ... do your work, commit ...
muse code add .
muse commit -m "feat(audio): implement WAV export" --agent-id claude-code --model-id claude-sonnet-4-6 --sign
# Tag the release candidate
muse tag add v0.3.0-rc1 -m "RC1: WAV export, no MP3 yet"
# Confirm it's there
muse tag list --pre --json | jq '.tags[].tag'
# "v0.3.0-rc1"
# "v0.2.0-rc14"
# "v0.1.0"
# Describe HEAD position
muse describe --pre --json
# { "tag": "v0.3.0-rc1", "distance": 0, "description": "v0.3.0-rc1", "exact": true }
# Merge to dev
muse checkout dev
muse merge task/audio-export
muse branch -d task/audio-export
# Push branch + all tags together
muse push local dev --tags
# Confirm tags arrived on the remote
muse -C ~/repos/clone tag list --pre --json | jq '.tags[].tag'
# "v0.3.0-rc1"
# "v0.2.0-rc14"
# "v0.1.0"
Label annotations vs version tags — when to use which
Use muse label when the annotation is semantic metadata about content:
muse label add emotion:joyful
muse label add section:chorus
muse label add tempo:allegro
Use muse tag when you are marking a point in history with a version number:
muse tag add v0.2.0-rc14
muse tag add v1.0.0
They live in different directories (.muse/tags/ vs .muse/version-tags/), sort by
different keys, and push through different channels. They do not interfere.
Error cases
Invalid version tag name:
$ muse tag add emotion:joyful --json
{
"exit_code": 1,
"error": "invalid_version_tag_name",
"message": "Version tag must start with 'v' (e.g. 'v0.2.0-rc14'), got: 'emotion:joyful'"
}
$ muse tag add v1.0 --json
{
"exit_code": 1,
"error": "invalid_version_tag_name",
"message": "Version 'v1.0' is not valid semver (expected vMAJOR.MINOR.PATCH[-pre][+build])."
}
Tag not found:
$ muse tag read v9.9.9 --json
{
"exit_code": 1,
"error": "version_tag_not_found",
"tag": "v9.9.9"
}
Remote unreachable:
$ muse tag push offline v0.1.0 --json
{
"exit_code": 1,
"error": "remote_not_found",
"remote": "offline"
}
Quick reference
muse tag add v1.0.0 # tag HEAD
muse tag add v1.0.0 feat/audio # tag branch tip
muse tag add v1.0.0 -m "stable" # with annotation
muse tag add v1.0.0 --dry-run # validate without writing
muse tag add v1.0.0 --force # overwrite existing tag
muse tag list # stable only, semver-descending
muse tag list --pre # include pre-release
muse tag list --sort created # newest created first
muse tag read v1.0.0 # full record
muse tag delete v1.0.0 # remove (idempotent)
muse tag latest # highest stable tag
muse tag latest --pre # highest overall tag
muse tag push local v1.0.0 # push one tag
muse tag push local v1.0.0 --force # force-overwrite remote tag
muse push local dev --tags # push branch + all tags
muse config set push.tags true # always include tags on push
muse describe # stable tags only
muse describe --pre # include RC tags
muse describe <ref> # describe from branch or commit
muse describe --abbrev 12 # 12-char hex suffix
muse describe --json # machine-readable