You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/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
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).
Have gbrain autopilot running (it can be inadvertently started by gbrain autopilot status — see linked issue).
Run /sync-gbrain in a Claude Code session inside an indexed repo.
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).
Concurrently, autopilot's sync cycle picks up the same source and calls gbrain sync --strategy code → validateRepoState → recloneIfMissing → ungated rm -rf src.local_path.
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) #1731 — spawnSync 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 463spawnGbrain(["sources","remove",legacyId,"--confirm-destructive"], ...)// line 484 / 664spawnGbrain(["sync","--strategy","code","--source",sourceId], ...)// line 723spawnGbrain(["reindex-code","--source",sourceId,"--yes"], ...)// line 741spawnGbrain(["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 ungatedrm -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"))){returnstageError("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.
Adjacent: Windows: gstack-gbrain-sync.ts stage 3 fails to spawn gstack-brain-sync (spawnSync needs shell: true) #1731 — "Windows: gstack-gbrain-sync.ts stage 3 fails to spawn gstack-brain-sync (spawnSync needs shell: true)". This explains the brain-sync exited undefined symptom observed in this incident's transcript independently of destruction. The two bugs are present simultaneously on Windows; fixing one doesn't fix the other.
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.
/sync-gbraincan trigger gbrain's destructive auto-recovery, wiping user reposSeverity: 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-gbrainskill orchestrator (gstack-gbrain-sync.ts) wipes user working trees under a specific combination of conditions: (a) a stale legacy source row withconfig.remote_urlset exists in the gbrain DB, (b) the gbrainautopilotdaemon is running, and (c) the orchestrator runs its hostname-fold migration concurrently with autopilot's sync cycle. Recovery requires a freshgit cloneand 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
remote_urlin config (e.g., from an older/sync-gbrainthat used a URL-based registration flow).gbrain autopilotrunning (it can be inadvertently started bygbrain autopilot status— see linked issue)./sync-gbrainin a Claude Code session inside an indexed repo.gbrain sources remove <legacy-id> --confirm-destructive(line 484 / 664 ofgstack-gbrain-sync.ts).gbrain sync --strategy code→validateRepoState→recloneIfMissing→ ungatedrm -rf src.local_path.brain-syncstage exits withstatus undefined. Note on evidence weight: the brain-sync spawn failure has an independent cause (Windows:gstack-gbrain-sync.tsstage 3 fails to spawngstack-brain-sync(spawnSyncneedsshell: true) #1731 —spawnSyncmissingshell: trueon 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.tsruns these gbrain subcommands in code-stage order:Two of these reach destructive code in gbrain:
sources remove --confirm-destructivecallsremoveSource()in gbrain's sources-ops.ts:510, which containsrmSync(src.local_path, …)at line 570. That site is gated byisPathContained(local_path, ~/.gbrain/clones)— which happens to fail closed on Windows forE:\paths due to a separator-mismatch bug inisPathContained. On macOS/Linux, this gate could fire if the legacy source'slocal_pathwas inside~/.gbrain/clones/.sync --strategy codereachesrecloneIfMissing()in gbrain's sources-ops.ts:645, which contains an ungatedrm -rf src.local_pathat line 674.The orchestrator has no protection against either path: no
--keep-storageflag passed tosources remove, no pre-flight check that the source'sremote_urlisn'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: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. Ifconfig.remote_urlis set ANDlocal_pathis not inside~/.gbrain/clones/, refuse the remove and surface a manual-fix instruction.3. Pass
--keep-storageto allgbrain sources removecalls 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
recloneIfMissingwould be reached. Before callinggbrain sync --strategy code, query the source row's config. Ifremote_urlis set, surface a warning ("source is URL-managed; sync may auto-reclone") and require an explicit--allow-recloneflag from the user to proceed.5. Update the user-facing
/sync-gbrainskill description and CLAUDE.md guidance to warn that this skill should not be run while autopilot is active, and to recommendgbrain sources add --path(no--url) for user-managed repos.Why this isn't only gbrain's problem to fix
Even with gbrain's
recloneIfMissingpatched, the orchestrator's pattern ofsources remove --confirm-destructive+ concurrent autopilot + race conditions is structurally risky. gstack should treat gbrain's destructive APIs the way one would treatrm -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
recloneIfMissingrm-rfs user-managed source path; ungated against arbitrary local_path gbrain#1526 —recloneIfMissingungated rm-rf (the actual destructive code).gbrain autopilot statusstarts the daemon instead of reporting status gbrain#1525 —gbrain autopilot statusUX trap (how autopilot inadvertently starts).gstack-gbrain-sync.tsstage 3 fails to spawngstack-brain-sync(spawnSyncneedsshell: true) #1731 — "Windows:gstack-gbrain-sync.tsstage 3 fails to spawngstack-brain-sync(spawnSyncneedsshell: true)". This explains thebrain-sync exited undefinedsymptom observed in this incident's transcript independently of destruction. The two bugs are present simultaneously on Windows; fixing one doesn't fix the other./sync-gbrain --fullon big brains; no resume from import-checkpoint #1611 — "Memory-ingest has hardcoded 35-min timeout that SIGTERMs/sync-gbrain --fullon big brains". Different bug, same orchestrator + same Windows context.Suggested release-note language for the gstack fix