bug: muse bridge git-export/import deletes ignored local files (.env, config/local.yaml) when --git-dir is a working tree — still present in 0.2.0rc15
Summary
muse bridge git-export (and git-import) with --git-dir pointed at a working tree permanently
deletes ignored local files — .env, config/local.yaml, data/, local databases — because the
exporter deletes every file under the target that is not on a hardcoded/--exclude list, without
consulting git check-ignore, .gitignore, or .museignore. Those files were never in the Muse
snapshot, so they are unrecoverable. This is a silent data-loss + secret-management risk.
First reported 2026-05-20 against 0.2.0rc7 (could not be filed upstream at the time — MuseHub API error 401: No registered keys for identity). Re-filing now: confirmed still present through
0.2.0rc15. RC14/RC15 neither introduced nor fixed it.
Why re-file now
A second production incident occurred (~2026-07-02, a repo mirrored via the bridge): .env and
config/local.yaml were deleted again and had to be recreated from a private backup.
Evidence gathered 2026-07-03 (Muse 0.2.0rc15)
| Test | Result | Meaning |
|---|---|---|
Isolated bridge into a dedicated --git-dir + --exclude (safe wrapper) |
PASS | The isolated-checkout pattern still protects ignored files under rc15. |
muse checkout / checkout -b / code add . against ignored .env, config/local.yaml (throwaway repo) |
files SURVIVED | Everyday commands are safe; they are not the deleter. |
Raw muse bridge git-export --git-dir . … run by hand against the dev tree |
ignored files DELETED | The destructive raw form against a working tree is the cause. |
Conclusion (verified, not inferred): the deletions are caused solely by running
muse bridge git-export (or git-import) with --git-dir pointed at a working tree.
GitExporter.sync_to_git deletes every file under the target that is not on a hardcoded/--exclude
list, without consulting ignore rules. Ignored secrets are therefore deleted and are unrecoverable.
Reproduction (throwaway repo — safe)
tmp=$(mktemp -d); cd "$tmp"; git init -q
muse init --domain code
printf '.muse/\n.env\nconfig/local.yaml\n' > .gitignore
printf '[global]\npatterns=[".env","config/local.yaml"]\n[domain.code]\npatterns=[]\n' > .museignore
mkdir config; echo v1 > tracked.txt
muse code add tracked.txt .gitignore .museignore; muse commit -m seed
git checkout -b muse-mirror -q; git commit --allow-empty -qm seed; git checkout -q main || git checkout -qb main
echo 'SECRET=keep' > .env; echo 'x: keep' > config/local.yaml
muse bridge git-export --git-dir . --git-branch muse-mirror --git-remote origin --no-push
ls .env config/local.yaml # EXPECTED (post-fix): both still exist. ACTUAL (rc15): both deleted.
Requested fix
- Delete only bridge-owned paths — persist the previous export manifest; delete candidates are
owned_before − owned_now, never arbitrary files inside--git-dir. - Respect ignore rules before unlinking — batched
git check-ignore --stdin(and.museignore); skip ignored paths unless the caller passes an explicit--allow-delete-ignored. - Add a regression test asserting that an ignored, untracked
.env/config/local.yamlsurvives agit-export/git-importrun against a populated--git-dir.
Impact
- Severity: Critical — silent local data loss + secret-management risk.
- Affected:
muse/cli/commands/bridge.py→GitExporter.sync_to_git(delete pass). - Verified in:
0.2.0rc7(original report) and0.2.0rc15(this re-file).
Workaround in place downstream
Never point a bridge command at a working tree. Mirror only via an isolated checkout
(dedicated --git-dir + --exclude for every ignore pattern), plus a shell guard that refuses
muse bridge git-export/import --git-dir .. This fully mitigates locally but does not remove the
upstream footgun for other users.
Fixed and merged to
devvia proposal #8 (sha256:b7be56ec).Root cause:
GitExporter.sync_to_git's delete pass swept every file under--git-dirand unlinked anything not on a hardcoded/--excludelist, with no check for whether Muse had ever actually written that path.Fix (both parts of the requested fix, implemented):
owned_before − owned_now, never an unconditional sweep. A path Muse never wrote can't appear inowned_before, so it's protected on the first export and every one after — this alone already fixes the exact reproduction in this ticket, since.env/config/local.yamlwere never in any prior export's manifest.git check-ignore --stdin(batched) +.museignoreglobal patterns checked against the (now much smaller) delete-candidate set; skipped unless--allow-delete-ignored.One finding worth recording:
git check-ignorenever reports a path as ignored while it's still tracked in the git index (correct git behavior — tracked overrides ignore everywhere in git). A path that was previously tracked by the bridge and then reused needs.museignore, not.gitignore, for ignore-based protection until something actually untracks it. Documented directly inGitExporter._ignored_paths's docstring so this doesn't get rediscovered as a mystery later.Scope correction: investigated
git-importdirectly — it has no delete pass at all, ever. It only replays git commits into the Muse object store and never touches git working-tree files. The "(and git-import)" in this ticket's title doesn't correspond to any code path that exists; this fix is correctly scoped to the exporter alone.Verification: ran this ticket's exact reproduction script against the fix — both
.envandconfig/local.yamlsurvive. 5 new regression tests (TestIgnoredFileProtection), all 282 existing bridge tests still pass.