# Local Dev Safety: Isolating `muse`'s Dev Build From Its Own Object Store Tracking issue: musehub staging [#185](https://staging.musehub.ai/gabriel/musehub/issues/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 any `muse`-like executable as editable (live source) or installed, reporting `path`, `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 bash` shims) 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 real `pip install` needed), 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-named `muse-dev` (explicit, editable) so there is never ambiguity about which build is running. - **Phase 3**: a guard rail so `muse-dev` refuses mutating commands against `~/ecosystem/muse` or `~/ecosystem/musehub` without an explicit `MUSE_DEV_ALLOW_CANONICAL=1` override. - **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 bundle` archives), both rotated, plus a restore script. - **Phase 6**: wire it together, update `agent-guide.md`, retire the manual `~/dev/backups` full-copy habit once the new pipeline is proven. See #185 for the full multiphase plan and rationale.