Summary
performFullSync (src/commands/sync.ts:281-302) does not honor
opts.dryRun. Calling sync_brain with dry_run: true on a fresh repo
(first sync / full sync path) still writes pages to the brain — the flag
is never passed into the runImport call chain. Incremental syncs honor
the flag correctly (src/commands/sync.ts:137-156), so this is a
first-sync / full-sync-only gap.
// src/commands/sync.ts:281-302 — performFullSync ignores opts.dryRun
async function performFullSync(
engine: BrainEngine,
repoPath: string,
headCommit: string,
opts: SyncOpts,
): Promise<SyncResult> {
console.log(`Running full import of ${repoPath}...`);
const { runImport } = await import('./import.ts');
const importArgs = [repoPath];
if (opts.noEmbed) importArgs.push('--no-embed');
await runImport(engine, importArgs); // ← opts.dryRun never checked, never passed
await engine.setConfig('sync.last_commit', headCommit);
// ...
}
Compare to the incremental sync path (sync.ts:137-156), which short-circuits
cleanly when dry_run is set:
if (opts.dryRun) {
console.log(`Sync dry run: ${lastCommit.slice(0, 8)}..${headCommit.slice(0, 8)}`);
// ... prints what would change, returns status: 'dry_run' without any writes
}
Location
src/commands/sync.ts:281-302 (performFullSync — flag ignored)
src/commands/sync.ts:137-156 (incremental path — flag honored, for comparison)
Reproduction
Verified against master HEAD b7e3005 (v0.10.1).
mkdir /tmp/test-repo && cd /tmp/test-repo
git init -q
echo "# test page" > page.md
git add -A && git commit -qm init
# Fresh repo → full sync path → dry_run ignored
gbrain call sync_brain '{"repo":"/tmp/test-repo","dry_run":true,"no_pull":true,"no_embed":true}'
# → "Running full import of /tmp/test-repo... 1 pages imported"
# Second call confirms writes persisted: a real dry run would have left the brain untouched,
# so this call would also report "first_sync" again. Instead it returns "up_to_date":
gbrain call sync_brain '{"repo":"/tmp/test-repo","dry_run":true,"no_pull":true,"no_embed":true}'
# → { "status": "up_to_date" }
The first call wrote to the brain despite dry_run: true. The second call sees the
persisted checkpoint and reports up_to_date, confirming the first call was not a
dry run.
Suggested fix
Pass opts.dryRun into the runImport call chain at sync.ts:281-302 and
short-circuit persistence in the chunker / DB-write layer when set.
Minimal-scope alternative — mirror the incremental short-circuit at the top of
performFullSync:
if (opts.dryRun) {
const { isSyncable } = await import('../core/sync.ts');
// enumerate candidate files without writing, return { status: 'dry_run', ... }
return { status: 'dry_run', fromCommit: null, toCommit: headCommit, /* ... */ };
}
Metadata
|
|
| Severity |
Low |
| Category |
Advisory flag bypass (correctness) |
| Confidence |
High (runtime-verified) |
| Affected version |
v0.10.1 (master HEAD b7e3005) |
| Reachable via |
MCP sync_brain, CLI gbrain call sync_brain |
[2026-04-16 updated] This issue originally combined the dry_run bug above
with a broader framing about sync_brain accepting arbitrary caller-supplied
git repo paths. On reflection that framing was too broad — repo is a
documented parameter and "the MCP caller supplies a path to sync" is the
tool's intended behavior, not an authorization bypass. I've narrowed this
issue to just the dry_run bypass, which is an unambiguous correctness bug.
Apologies for the initial framing breadth.
Summary
performFullSync(src/commands/sync.ts:281-302) does not honoropts.dryRun. Callingsync_brainwithdry_run: trueon a fresh repo(first sync / full sync path) still writes pages to the brain — the flag
is never passed into the
runImportcall chain. Incremental syncs honorthe flag correctly (
src/commands/sync.ts:137-156), so this is afirst-sync / full-sync-only gap.
Compare to the incremental sync path (
sync.ts:137-156), which short-circuitscleanly when
dry_runis set:Location
Reproduction
Verified against master HEAD
b7e3005(v0.10.1).The first call wrote to the brain despite
dry_run: true. The second call sees thepersisted checkpoint and reports
up_to_date, confirming the first call was not adry run.
Suggested fix
Pass
opts.dryRuninto therunImportcall chain atsync.ts:281-302andshort-circuit persistence in the chunker / DB-write layer when set.
Minimal-scope alternative — mirror the incremental short-circuit at the top of
performFullSync:Metadata
b7e3005)sync_brain, CLIgbrain call sync_brain