gabriel / muse public
omzsh-plugin.md markdown
219 lines 6.2 KB
Raw
sha256:cb2da6c61116ad1ab98d03747c21d6f66485839c7b4efd7d0124db0f8aa14e41 refactor(harmony): move auto_apply + record_resolutions int… Sonnet 4.6 minor ⚠ breaking 24 days ago

Oh My ZSH Plugin — Reference

Minimal, secure ZSH integration for Muse. Provides a prompt segment, core aliases, and tab completion. Nothing runs automatically beyond what is needed to keep the prompt accurate.


Files

File Purpose
tools/omzsh-plugin/muse.plugin.zsh Main plugin (~230 lines)
tools/omzsh-plugin/_muse ZSH completion function
tools/install-omzsh-plugin.sh Symlink installer

Prompt segment

# In ~/.zshrc
PROMPT='%~ $(muse_prompt_info) %# '

muse_prompt_info emits nothing outside a Muse repo. Inside one it emits:

%F{cyan}muse:(%F{<color>}<domain>:<branch>%F{cyan})%f

The inner color is the only dirty signal — no extra symbol or count:

  • magenta — working tree is clean
  • yellow — uncommitted changes exist

This mirrors the Git plugin's git:(branch) format, extended with the domain name — the key differentiator from a single-domain VCS.

Examples

Output Inner color Meaning
muse:(midi:main) magenta clean
muse:(midi:main) yellow uncommitted changes
muse:(midi:a1b2c3d4) magenta detached HEAD, clean
muse:(midi:a1b2c3d4) yellow detached HEAD, uncommitted changes

The yellow state reflects the working tree at the time of the last cd, shell load, or muse command.

Domain icons (optional)

Icons are off by default. Set MUSE_PROMPT_ICONS=1 to prepend one:

Domain Icon Config key
midi MUSE_DOMAIN_ICONS[midi]
code MUSE_DOMAIN_ICONS[code]
bitcoin MUSE_DOMAIN_ICONS[bitcoin]
scaffold MUSE_DOMAIN_ICONS[scaffold]
(unknown) MUSE_DOMAIN_ICONS[_default]

With icons on, the prompt becomes ♪ muse:(midi:main).


Environment variables

Configuration (set before plugins=(… muse …))

Variable Default Meaning
MUSE_PROMPT_ICONS 0 1 prepends a domain icon before muse:(…)
MUSE_DIRTY_TIMEOUT 1 Seconds before dirty check aborts

State (read-only, exported by plugin)

Variable Type Meaning
MUSE_REPO_ROOT string Absolute path to repo root, or ""
MUSE_DOMAIN string Active domain name
MUSE_BRANCH string Branch name, short SHA, or ?
MUSE_DIRTY integer 1 if working tree has changes
MUSE_DIRTY_COUNT integer Number of changed paths

Hooks

Hook When it fires What it does
chpwd On cd Full refresh: re-finds root, re-reads HEAD, domain, and dirty state
preexec Before any command Sets _MUSE_CMD_RAN=1 when command is muse
precmd Before prompt Runs full refresh only if _MUSE_CMD_RAN=1

Aliases

Alias Expands to
mst muse status
msts muse status --short
mcm muse commit -m
mco muse checkout
mlg muse log
mlgo muse log --oneline
mlgg muse log --graph
mdf muse diff
mdfst muse diff --stat
mbr muse branch
mtg muse tag
mfh muse fetch
mpull muse pull
mpush muse push
mrm muse remote

Completion

The _muse completion function handles:

  • Top-level command names with descriptions.
  • Branch names for checkout, merge, cherry-pick, branch, reset, revert, diff, show, blame.
  • Remote names for push, pull, fetch.
  • Tag names for tag.
  • Config key suggestions for config.
  • Subcommand names for shelf, remote, plumbing, commit flags.

All branch/tag/remote lookups use ZSH glob patterns against .muse/refs/ and .muse/remotes/ — no subprocess, no ls, instant.


Performance model

Trigger Subprocesses What runs
Prompt render 0 Reads cached shell vars only
cd into repo 2 (python3 + muse) HEAD (ZSH read) + domain + dirty check
cd outside repo 0 Clears vars only
Shell load / exec zsh 2 (python3 + muse) Same as cd into repo
After muse command 2 (python3 + muse) Full refresh + dirty check
Tab completion 0 ZSH glob reads .muse/refs/

Security model

Branch name injection

.muse/HEAD is read with a pure ZSH $(<file) — no subprocess. Muse writes HEAD in one of two self-describing forms (set by muse/core/store.py):

ref: refs/heads/<branch>   — on a branch
commit: <sha256>           — detached HEAD

The branch name is validated with [[ "$branch" =~ '^[[:alnum:]/_.-]+$' ]]. Any name containing characters outside this set (including %, $, backticks, quotes) is replaced with ?. Valid names are additionally %-escaped (${branch//\%/%%}) before insertion into the prompt string, so ZSH never interprets them as prompt directives.

Domain injection

The domain value from .muse/repo.json is extracted by python3 and validated with safe.isalnum() and 1 <= len(v) <= 32 before printing. The path to repo.json is passed via the MUSE_REPO_JSON environment variable — never interpolated into a -c string — so a path containing single quotes, spaces, or special characters is handled safely.

Path injection

cd -- "$MUSE_REPO_ROOT" uses -- so the path cannot be interpreted as a flag. timeout -- ... follows the same pattern.

No eval

No user-supplied data is ever passed to eval.

Completion safety

The completion function uses ZSH glob expansion (${refs_dir}/*(N:t)) instead of $(ls ...) to enumerate branches. This avoids word-splitting on filenames that contain spaces, and prevents ls output from being treated as shell tokens.


Installation

bash tools/install-omzsh-plugin.sh

The script creates a symlink from ~/.oh-my-zsh/custom/plugins/muse/ to tools/omzsh-plugin/. Because it is a symlink, pulling new commits to the Muse repo automatically updates the plugin.

Add to ~/.zshrc:

plugins=(git muse)

Then add the prompt segment:

# Append to your existing PROMPT, or set a new one:
PROMPT+='$(muse_prompt_info) '

Then reload:

exec zsh
File History 2 commits
sha256:cb2da6c61116ad1ab98d03747c21d6f66485839c7b4efd7d0124db0f8aa14e41 refactor(harmony): move auto_apply + record_resolutions int… Sonnet 4.6 minor 24 days ago
sha256:596a4963c21debb14d9ef51e23c2ca9f825b602ab8585f69caca35eb81bcac77 chore(harmony): baseline audit — Phase 0 of issue #16 Sonnet 4.6 28 days ago