Skip to content

/sync-gbrain can trigger gbrain's destructive auto-recovery, wiping user repos #1734

@Oszkar

Description

@Oszkar

/sync-gbrain can trigger gbrain's destructive auto-recovery, wiping user repos

Severity: Critical / data loss (downstream of gbrain bug)
Repo: garrytan/gstack
Affected version: v1.45.0.0 (orchestrator at ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts, mtime 2026-05-23)
Dependency: gbrain v0.41.14.0 (the destructive path lives there, see linked issue)

Summary

The /sync-gbrain skill orchestrator (gstack-gbrain-sync.ts) wipes user working trees under a specific combination of conditions: (a) a stale legacy source row with config.remote_url set exists in the gbrain DB, (b) the gbrain autopilot daemon is running, and (c) the orchestrator runs its hostname-fold migration concurrently with autopilot's sync cycle. Recovery requires a fresh git clone and any uncommitted work on un-pushed branches is lost.

I lost the working tree to this bug on 2026-05-27 — .git/, backend/, docs/, frontend/*, and root config files all wiped. GitHub remote was intact, recovered via fresh clone.

This issue is the gstack-side companion to the upstream gbrain data-loss bug. Even after gbrain fixes its destructive paths, gstack should defend itself by avoiding calls into known-unsafe gbrain operations and by detecting/halting concurrent autopilot.

Reproduction sequence

  1. Have gbrain v0.41.14.0 installed and a source previously registered with remote_url in config (e.g., from an older /sync-gbrain that used a URL-based registration flow).
  2. Have gbrain autopilot running (it can be inadvertently started by gbrain autopilot status — see linked issue).
  3. Run /sync-gbrain in a Claude Code session inside an indexed repo.
  4. The orchestrator computes a new source ID (hostname-fold migration) and calls gbrain sources remove <legacy-id> --confirm-destructive (line 484 / 664 of gstack-gbrain-sync.ts).
  5. Concurrently, autopilot's sync cycle picks up the same source and calls gbrain sync --strategy codevalidateRepoStaterecloneIfMissing → ungated rm -rf src.local_path.
  6. The user's working tree is destroyed mid-orchestrator. The orchestrator's later brain-sync stage exits with status undefined. Note on evidence weight: the brain-sync spawn failure has an independent cause (Windows: gstack-gbrain-sync.ts stage 3 fails to spawn gstack-brain-sync (spawnSync needs shell: true) #1731spawnSync missing shell: true on Windows), so it can't be used as direct evidence that destruction happened. The destructive action is evidenced by the survivor pattern (selectively deleted tracked files plus locked-dir partial-failure remnants) and the transcript timeline, not by brain-sync's failure.

Forensic transcript can be attached if needed.

What the orchestrator does that contributes to the risk

gstack-gbrain-sync.ts runs these gbrain subcommands in code-stage order:

spawnGbrain(["sources", "rename", legacyId, newSourceId], ...)           // line 463
spawnGbrain(["sources", "remove", legacyId, "--confirm-destructive"], ...)  // line 484 / 664
spawnGbrain(["sync", "--strategy", "code", "--source", sourceId], ...)   // line 723
spawnGbrain(["reindex-code", "--source", sourceId, "--yes"], ...)        // line 741
spawnGbrain(["sources", "attach", sourceId], ...)                        // line 768

Two of these reach destructive code in gbrain:

  • sources remove --confirm-destructive calls removeSource() in gbrain's sources-ops.ts:510, which contains rmSync(src.local_path, …) at line 570. That site is gated by isPathContained(local_path, ~/.gbrain/clones) — which happens to fail closed on Windows for E:\ paths due to a separator-mismatch bug in isPathContained. On macOS/Linux, this gate could fire if the legacy source's local_path was inside ~/.gbrain/clones/.
  • sync --strategy code reaches recloneIfMissing() in gbrain's sources-ops.ts:645, which contains an ungated rm -rf src.local_path at line 674.

The orchestrator has no protection against either path: no --keep-storage flag passed to sources remove, no pre-flight check that the source's remote_url isn't set, no detection of running autopilot.

What gstack should do, even before gbrain ships its fix

1. Refuse to operate when autopilot is running. Before the code stage, check ~/.gbrain/autopilot.lock:

if (existsSync(join(gbrainHome, "autopilot.lock"))) {
  return stageError("autopilot running; refusing to run /sync-gbrain concurrently. Stop autopilot first with: kill $(cat ~/.gbrain/autopilot.lock) && rm ~/.gbrain/autopilot.lock");
}

2. Audit legacy source rows before any hostname-fold migration. Before calling gbrain sources remove --confirm-destructive, query the DB for the legacy source's full config. If config.remote_url is set AND local_path is not inside ~/.gbrain/clones/, refuse the remove and surface a manual-fix instruction.

3. Pass --keep-storage to all gbrain sources remove calls in the orchestrator. The orchestrator never wants gbrain to touch the source repo's files — it only wants the DB row gone. The flag exists; pass it.

4. Detect and short-circuit if recloneIfMissing would be reached. Before calling gbrain sync --strategy code, query the source row's config. If remote_url is set, surface a warning ("source is URL-managed; sync may auto-reclone") and require an explicit --allow-reclone flag from the user to proceed.

5. Update the user-facing /sync-gbrain skill description and CLAUDE.md guidance to warn that this skill should not be run while autopilot is active, and to recommend gbrain sources add --path (no --url) for user-managed repos.

Why this isn't only gbrain's problem to fix

Even with gbrain's recloneIfMissing patched, the orchestrator's pattern of sources remove --confirm-destructive + concurrent autopilot + race conditions is structurally risky. gstack should treat gbrain's destructive APIs the way one would treat rm -rf: opt-in flags, pre-flight checks, explicit user confirmation, and refuse-to-operate guards. Right now the orchestrator calls them like they're safe.

Cross-references

Suggested release-note language for the gstack fix

/sync-gbrain now refuses to run when gbrain autopilot is active, audits source-row configs before any destructive gbrain operation, and passes --keep-storage to gbrain sources remove. This closes a class of data-loss bugs where the orchestrator could race autopilot into rm-rf'ing the indexed working tree on Windows. If you previously ran /sync-gbrain and aren't sure whether your indexed repos are safe, audit: psql -d <brain> -c "SELECT id, local_path, config->>'remote_url' FROM sources" — any row with non-null remote_url is at risk.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions