gabriel / muse public
filed by aaronrene human · 112 days ago

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

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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

  1. Delete only bridge-owned paths — persist the previous export manifest; delete candidates are owned_before − owned_now, never arbitrary files inside --git-dir.
  2. Respect ignore rules before unlinking — batched git check-ignore --stdin (and .museignore); skip ignored paths unless the caller passes an explicit --allow-delete-ignored.
  3. Add a regression test asserting that an ignored, untracked .env / config/local.yaml survives a git-export/git-import run 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) and 0.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.

◎ Activity1
●
aaronrene opened this issue 112 days ago
○
gabriel 111 days ago

Fixed and merged to dev via proposal #8 (sha256:b7be56ec).

Root cause: GitExporter.sync_to_git's delete pass swept every file under --git-dir and unlinked anything not on a hardcoded/--exclude list, with no check for whether Muse had ever actually written that path.

Fix (both parts of the requested fix, implemented):

  1. Ownership tracking — the manifest paths from the previous successful export are now persisted in a sidecar JSON file. Delete candidates are owned_before − owned_now, never an unconditional sweep. A path Muse never wrote can't appear in owned_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.yaml were never in any prior export's manifest.
  2. Ignore-rule defense in depth — git check-ignore --stdin (batched) + .museignore global patterns checked against the (now much smaller) delete-candidate set; skipped unless --allow-delete-ignored.

One finding worth recording: git check-ignore never 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 in GitExporter._ignored_paths's docstring so this doesn't get rediscovered as a mystery later.

Scope correction: investigated git-import directly — 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 .env and config/local.yaml survive. 5 new regression tests (TestIgnoredFileProtection), all 282 existing bridge tests still pass.

✓
closed this issue 111 days ago