gabriel / muse public
filed by aaronrene human · 19 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.pyGitExporter.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 19 days ago
gabriel 18 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 depthgit 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 18 days ago