# `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
```bash
# 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
```