The Problem
This one had already caused production data loss before I caught it. The blog sync CLI (bun run sync:blog) has a reconciliation phase that pulls statuses from PocketBase back into Obsidian frontmatter -- the idea being that production status always wins. An admin marks a post as "published" in production, and reconciliation writes that status back to the markdown file.
The bug: reconciliation runs against whichever PocketBase instance PB_URL points to. When I synced to my local dev instance, it would fetch the local statuses (often stale "draft" values), write them into frontmatter, and the next production sync would push those corrupted statuses upstream. A post I had published would silently revert to draft.
Root Cause
The reconciliation feature was designed with a single-instance mental model. The code at index.ts:570 checks if (!noPull && syncClient !== null) and runs reconciliation -- but it assumes the target PocketBase IS production. There was no mechanism to distinguish a local dev instance from production, and --no-pull existed but its relationship to multi-instance safety was not documented or obvious.
The Fix
I added a --local flag to the CLI. When passed, it sets local: true in the config and forces noPull: true, completely skipping the reconciliation phase. The implementation reuses the existing noPull code path rather than adding a parallel one.
The key change was small but important:
- Added
local: booleanto theSyncConfigtype - In
buildSyncConfig(), the--localflag sets bothlocal: trueandnoPull: true - Added help text explaining the flag: "Skip status reconciliation -- use when syncing to a local development PocketBase instance to prevent frontmatter overwrites that could corrupt production"
- Added a log line:
[cli] Local mode: reconciliation disabled (--local flag)
I also extracted buildSyncConfig() as a pure function (separating it from the process.exit() calls in parseCLIArgs()) so it could be properly unit tested.
Testing
Defensive TDD again. Safety net tests confirmed all existing flags still work. Reproduction tests asserted that --local produces the right config shape and blocks reconciliation. The integration test simulated the exact multi-instance conflict scenario with temp markdown files and a mocked local PocketBase returning stale statuses.
Lessons
The --local flag is semantic sugar over --no-pull, but intent matters. A developer running --local communicates "I am syncing to dev" far more clearly than remembering to pass --no-pull every time. The footgun was not that the mechanism was missing -- it was that the safe path was not the obvious path.