Local Dev Safety: Isolating muse's Dev Build From Its Own Object Store
Tracking issue: musehub staging #185.
The problem
~/ecosystem/muse is dogfooded: it's the source code we edit and a
muse-tracked repo with its own .muse/ object store (currently ~1.0GB) that
cannot be corrupted without real cost. ~/ecosystem/musehub has the same
shape (~809MB .muse/).
Before this doc existed, the only mitigation was a manual, ad hoc full-directory
copy to ~/dev/backups/{muse,musehub} before risky changes — no verification,
no rotation, easy to forget.
Phase 1 finding: the resolved muse binary is ambiguous, and the default is the risky one
Running scripts/dev/which_muse.py (this phase's audit tool) against every
muse-named binary reachable on this machine found five distinct
resolutions, not the "at least four" originally suspected when #185 was filed:
| Resolved via | Path | Editable? | Version | Source root |
|---|---|---|---|---|
which muse (interactive shell) |
/Users/gabriel/bin/muse → /Users/gabriel/ecosystem/muse/.venv/bin/muse |
yes | 0.2.1 | /Users/gabriel/ecosystem/muse |
| homebrew | /opt/homebrew/bin/muse |
yes | 0.2.1 | /Users/gabriel/ecosystem/muse |
| installer (getting-started docs) | /Users/gabriel/.local/bin/muse → ~/.local/share/muse/venv/bin/muse |
no | 0.2.0.dev1 | ~/.local/share/muse/venv/.../site-packages |
| pyenv shim | /Users/gabriel/.pyenv/shims/muse |
n/a — bash dispatcher, not directly classifiable | — | delegates to pyenv's active version |
pyenv-managed install (what the shim above dispatches to, and also what Python's own shutil.which resolves from this doc's own audit script) |
/Users/gabriel/.pyenv/versions/3.14.4/bin/muse |
no | 0.2.0rc15 | /Users/gabriel/.pyenv/versions/3.14.4/lib/python3.14/site-packages |
Three separate versions (0.2.1, 0.2.0.dev1, 0.2.0rc15) are simultaneously
installed and reachable depending on how muse is invoked. Worse: even this
audit tooling's own shutil.which("muse") (run from a Python subprocess)
resolved to a different binary than the interactive shell's which muse —
the two disagreed because PATH itself differs across invocation contexts
(interactive shell vs. tool/script subprocess). That is direct, reproduced
evidence of the exact hazard #185 exists to close: it is not safe to assume
"the muse that runs" is a fixed, known thing.
The binary that wins in an interactive terminal (/Users/gabriel/bin/muse) is
the editable one — code loaded live from ~/ecosystem/muse's own working
tree. So by default, every muse command run in a terminal on this machine —
including ones aimed at musehub, agentception, or any other repo via
-C — executes code that may be mid-edit, syntactically valid but
semantically broken, sourced from the exact repo it can also mutate.
A real bug this audit tooling itself hit
While building the classifier (scripts/dev/which_muse.py), its first
implementation had a genuine correctness bug worth recording here because it's
the same class of hazard as the ambiguity above: the introspection
subprocess inherited the caller's current working directory, and Python's
'' (cwd) sys.path entry is searched before PYTHONPATH. Since the
tool is naturally run from inside ~/ecosystem/muse (which has a muse/
subpackage sitting at its own root), that cwd entry silently shadowed
whatever binary was actually being classified — every call resolved to the
real editable checkout regardless of the --target given. Fixed by pinning
the introspection subprocess's cwd to a neutral temp directory. Caught by
the Phase 1 test suite (tests/test_which_muse_script.py), not by manual
inspection — exactly the point of writing tests first.
What Phase 1 delivers
scripts/dev/which_muse.py— classifies anymuse-like executable as editable (live source) or installed, reportingpath,resolved_path,editable,version,source_root. Handles both literal (#!/path/to/python3) and indirect (#!/usr/bin/env [-S] python3) shebangs, and fails with a specific, actionable error for non-Python dispatcher shims (e.g. pyenv's#!/usr/bin/env bashshims) rather than an opaque stack trace.scripts/dev/which-muse.sh— thin PATH-friendly wrapper (which-muse.sh --json).tests/test_which_muse_script.py— 8 tests, all fixture-based (no realpip installneeded), covering: editable classification, installed classification, symlink resolution, missing-package error,env-indirect shebangs, non-Python dispatcher shebangs, and the CLI's JSON output shape.
This is observation-only — no PATH changes, no new binaries, no guard rails yet. Those come in Phases 2 and 3 of #185.
What's next
- Phase 2: split
muse(always resolves to a vetted, non-editable install) from a distinctly-namedmuse-dev(explicit, editable) so there is never ambiguity about which build is running. - Phase 3: a guard rail so
muse-devrefuses mutating commands against~/ecosystem/museor~/ecosystem/musehubwithout an explicitMUSE_DEV_ALLOW_CANONICAL=1override. - Phase 4: disposable APFS copy-on-write sandbox clones for actually doing the dev work Phase 3 protects against.
- Phase 5: two independent automated backup mechanisms (fast local COW
snapshots + verified
muse bundlearchives), both rotated, plus a restore script. - Phase 6: wire it together, update
agent-guide.md, retire the manual~/dev/backupsfull-copy habit once the new pipeline is proven.
See #185 for the full multiphase plan and rationale.